mirror of
				https://github.com/2dust/v2rayN.git
				synced 2025-10-26 10:14:42 +00:00 
			
		
		
		
	Compare commits
	
		
			23 commits
		
	
	
		
			cba8a49d5c
			...
			5f093a9b01
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 5f093a9b01 | ||
|   | ab1dc45ed4 | ||
|   | 2d41272659 | ||
|   | e7f75010d3 | ||
|   | aa1ccdd01b | ||
|   | b17323c982 | ||
|   | 71dcd8d1de | ||
|   | dace865e6c | ||
|   | fad94e68d2 | ||
|   | c116aae242 | ||
|   | ff1c9093a2 | ||
|   | 8bb20c0ab8 | ||
|   | 17a3a516c7 | ||
|   | 8f0d7e54d8 | ||
|   | 2ab79afa02 | ||
|   | e29d292732 | ||
|   | 9ef228db1e | ||
|   | 34327532e6 | ||
|   | 8af6eda165 | ||
|   | f979d13109 | ||
|   | 6166b6c0e3 | ||
|   | 8c094dd976 | ||
|   | 5c4f485471 | 
					 43 changed files with 2526 additions and 396 deletions
				
			
		|  | @ -12,5 +12,9 @@ public enum EConfigType | |||
|     TUIC = 8, | ||||
|     WireGuard = 9, | ||||
|     HTTP = 10, | ||||
|     Anytls = 11 | ||||
|     Anytls = 11, | ||||
| 
 | ||||
|     Group = 1000, | ||||
|     PolicyGroup = 1001, | ||||
|     ProxyChain = 1002, | ||||
| } | ||||
|  |  | |||
|  | @ -2,8 +2,9 @@ namespace ServiceLib.Enums; | |||
| 
 | ||||
| public enum EMultipleLoad | ||||
| { | ||||
|     LeastPing, | ||||
|     Fallback, | ||||
|     Random, | ||||
|     RoundRobin, | ||||
|     LeastPing, | ||||
|     LeastLoad | ||||
| } | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ public enum EViewAction | |||
|     RoutingRuleDetailsWindow, | ||||
|     AddServerWindow, | ||||
|     AddServer2Window, | ||||
|     AddGroupServerWindow, | ||||
|     DNSSettingWindow, | ||||
|     RoutingSettingWindow, | ||||
|     OptionSettingWindow, | ||||
|  |  | |||
|  | @ -50,6 +50,7 @@ public class Global | |||
|     public const string DirectTag = "direct"; | ||||
|     public const string BlockTag = "block"; | ||||
|     public const string DnsTag = "dns-module"; | ||||
|     public const string BalancerTagSuffix = "-round"; | ||||
|     public const string StreamSecurity = "tls"; | ||||
|     public const string StreamSecurityReality = "reality"; | ||||
|     public const string Loopback = "127.0.0.1"; | ||||
|  |  | |||
|  | @ -357,6 +357,11 @@ public static class ConfigHandler | |||
|                 { | ||||
|                 } | ||||
|             } | ||||
|             else if (profileItem.ConfigType > EConfigType.Group) | ||||
|             { | ||||
|                 var profileGroupItem = await AppManager.Instance.GetProfileGroupItem(it.IndexId); | ||||
|                 await AddGroupServerCommon(config, profileItem, profileGroupItem, true); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await AddServerCommon(config, profileItem, true); | ||||
|  | @ -1074,6 +1079,35 @@ public static class ConfigHandler | |||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     public static async Task<int> AddGroupServerCommon(Config config, ProfileItem profileItem, ProfileGroupItem profileGroupItem, bool toFile = true) | ||||
|     { | ||||
|         var maxSort = -1; | ||||
|         if (profileItem.IndexId.IsNullOrEmpty()) | ||||
|         { | ||||
|             profileItem.IndexId = Utils.GetGuid(false); | ||||
|             maxSort = ProfileExManager.Instance.GetMaxSort(); | ||||
|         } | ||||
|         if (maxSort > 0) | ||||
|         { | ||||
|             ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1); | ||||
|         } | ||||
|         if (toFile) | ||||
|         { | ||||
|             await SQLiteHelper.Instance.ReplaceAsync(profileItem); | ||||
|             if (profileGroupItem != null) | ||||
|             { | ||||
|                 profileGroupItem.ParentIndexId = profileItem.IndexId; | ||||
|                 await ProfileGroupItemManager.Instance.SaveItemAsync(profileGroupItem); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(profileItem.IndexId); | ||||
|                 await ProfileGroupItemManager.Instance.SaveTo(); | ||||
|             } | ||||
|         } | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Compare two profile items to determine if they represent the same server | ||||
|     /// Used for deduplication and server matching | ||||
|  | @ -1145,7 +1179,7 @@ public static class ConfigHandler | |||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Create a custom server that combines multiple servers for load balancing | ||||
|     /// Create a group server that combines multiple servers for load balancing | ||||
|     /// Generates a configuration file that references multiple servers | ||||
|     /// </summary> | ||||
|     /// <param name="config">Current configuration</param> | ||||
|  | @ -1153,45 +1187,55 @@ public static class ConfigHandler | |||
|     /// <param name="coreType">Core type to use (Xray or sing_box)</param> | ||||
|     /// <param name="multipleLoad">Load balancing algorithm</param> | ||||
|     /// <returns>Result object with success state and data</returns> | ||||
|     public static async Task<RetResult> AddCustomServer4Multiple(Config config, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad) | ||||
|     public static async Task<RetResult> AddGroupServer4Multiple(Config config, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad, string? subId) | ||||
|     { | ||||
|         var indexId = Utils.GetMd5(Global.CoreMultipleLoadConfigFileName); | ||||
|         var configPath = Utils.GetConfigPath(Global.CoreMultipleLoadConfigFileName); | ||||
|         var result = new RetResult(); | ||||
| 
 | ||||
|         var result = await CoreConfigHandler.GenerateClientMultipleLoadConfig(config, configPath, selecteds, coreType, multipleLoad); | ||||
|         if (result.Success != true) | ||||
|         { | ||||
|             return result; | ||||
|         } | ||||
|         var indexId = Utils.GetGuid(false); | ||||
|         var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList()); | ||||
| 
 | ||||
|         if (!File.Exists(configPath)) | ||||
|         { | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         var profileItem = await AppManager.Instance.GetProfileItem(indexId) ?? new(); | ||||
|         profileItem.IndexId = indexId; | ||||
|         var remark = string.Empty; | ||||
|         if (coreType == ECoreType.Xray) | ||||
|         { | ||||
|             profileItem.Remarks = multipleLoad switch | ||||
|             remark = multipleLoad switch | ||||
|             { | ||||
|                 EMultipleLoad.Random => ResUI.menuSetDefaultMultipleServerXrayRandom, | ||||
|                 EMultipleLoad.RoundRobin => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, | ||||
|                 EMultipleLoad.LeastPing => ResUI.menuSetDefaultMultipleServerXrayLeastPing, | ||||
|                 EMultipleLoad.LeastLoad => ResUI.menuSetDefaultMultipleServerXrayLeastLoad, | ||||
|                 _ => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, | ||||
|                 EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing, | ||||
|                 EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback, | ||||
|                 EMultipleLoad.Random => ResUI.menuGenGroupMultipleServerXrayRandom, | ||||
|                 EMultipleLoad.RoundRobin => ResUI.menuGenGroupMultipleServerXrayRoundRobin, | ||||
|                 EMultipleLoad.LeastLoad => ResUI.menuGenGroupMultipleServerXrayLeastLoad, | ||||
|                 _ => ResUI.menuGenGroupMultipleServerXrayRoundRobin, | ||||
|             }; | ||||
|         } | ||||
|         else if (coreType == ECoreType.sing_box) | ||||
|         { | ||||
|             profileItem.Remarks = ResUI.menuSetDefaultMultipleServerSingBoxLeastPing; | ||||
|             remark = multipleLoad switch | ||||
|             { | ||||
|                 EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing, | ||||
|                 EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback, | ||||
|                 _ => ResUI.menuGenGroupMultipleServerSingBoxLeastPing, | ||||
|             }; | ||||
|         } | ||||
|         profileItem.Address = Global.CoreMultipleLoadConfigFileName; | ||||
|         profileItem.ConfigType = EConfigType.Custom; | ||||
|         profileItem.CoreType = coreType; | ||||
| 
 | ||||
|         await AddServerCommon(config, profileItem, true); | ||||
| 
 | ||||
|         var profile = new ProfileItem | ||||
|         { | ||||
|             IndexId = indexId, | ||||
|             CoreType = coreType, | ||||
|             ConfigType = EConfigType.PolicyGroup, | ||||
|             Remarks = remark, | ||||
|             Address = childProfileIndexId, | ||||
|         }; | ||||
|         if (!subId.IsNullOrEmpty()) | ||||
|         { | ||||
|             profile.Subid = subId; | ||||
|         } | ||||
|         var profileGroup = new ProfileGroupItem | ||||
|         { | ||||
|             ChildItems = childProfileIndexId, | ||||
|             MultipleLoad = multipleLoad, | ||||
|             ParentIndexId = indexId, | ||||
|         }; | ||||
|         var ret = await AddGroupServerCommon(config, profile, profileGroup, true); | ||||
|         result.Success = ret == 0; | ||||
|         result.Data = indexId; | ||||
|         return result; | ||||
|     } | ||||
|  |  | |||
|  | @ -132,24 +132,4 @@ public static class CoreConfigHandler | |||
|         await File.WriteAllTextAsync(fileName, result.Data.ToString()); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     public static async Task<RetResult> GenerateClientMultipleLoadConfig(Config config, string fileName, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad) | ||||
|     { | ||||
|         var result = new RetResult(); | ||||
|         if (coreType == ECoreType.sing_box) | ||||
|         { | ||||
|             result = await new CoreConfigSingboxService(config).GenerateClientMultipleLoadConfig(selecteds); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             result = await new CoreConfigV2rayService(config).GenerateClientMultipleLoadConfig(selecteds, multipleLoad); | ||||
|         } | ||||
| 
 | ||||
|         if (result.Success != true) | ||||
|         { | ||||
|             return result; | ||||
|         } | ||||
|         await File.WriteAllTextAsync(fileName, result.Data.ToString()); | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -67,6 +67,7 @@ public sealed class AppManager | |||
|         SQLiteHelper.Instance.CreateTable<ProfileExItem>(); | ||||
|         SQLiteHelper.Instance.CreateTable<DNSItem>(); | ||||
|         SQLiteHelper.Instance.CreateTable<FullConfigTemplateItem>(); | ||||
|         SQLiteHelper.Instance.CreateTable<ProfileGroupItem>(); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|  | @ -101,6 +102,7 @@ public sealed class AppManager | |||
| 
 | ||||
|             await ConfigHandler.SaveConfig(_config); | ||||
|             await ProfileExManager.Instance.SaveTo(); | ||||
|             await ProfileGroupItemManager.Instance.SaveTo(); | ||||
|             await StatisticsManager.Instance.SaveTo(); | ||||
|             await CoreManager.Instance.CoreStop(); | ||||
|             StatisticsManager.Instance.Close(); | ||||
|  | @ -225,6 +227,15 @@ public sealed class AppManager | |||
|         return await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(it => it.Remarks == remarks); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<ProfileGroupItem?> GetProfileGroupItem(string parentIndexId) | ||||
|     { | ||||
|         if (parentIndexId.IsNullOrEmpty()) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|         return await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().FirstOrDefaultAsync(it => it.ParentIndexId == parentIndexId); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<List<RoutingItem>?> RoutingItems() | ||||
|     { | ||||
|         return await SQLiteHelper.Instance.TableAsync<RoutingItem>().OrderBy(t => t.Sort).ToListAsync(); | ||||
|  |  | |||
							
								
								
									
										167
									
								
								v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,167 @@ | |||
| using System.Collections.Concurrent; | ||||
| 
 | ||||
| namespace ServiceLib.Manager; | ||||
| 
 | ||||
| public class ProfileGroupItemManager | ||||
| { | ||||
|     private static readonly Lazy<ProfileGroupItemManager> _instance = new(() => new()); | ||||
|     private ConcurrentDictionary<string, ProfileGroupItem> _items = new(); | ||||
| 
 | ||||
|     public static ProfileGroupItemManager Instance => _instance.Value; | ||||
|     private static readonly string _tag = "ProfileGroupItemManager"; | ||||
| 
 | ||||
|     private ProfileGroupItemManager() | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     public async Task Init() | ||||
|     { | ||||
|         await InitData(); | ||||
|     } | ||||
| 
 | ||||
|     // Read-only getters: do not create or mark dirty | ||||
|     public bool TryGet(string indexId, out ProfileGroupItem? item) | ||||
|     { | ||||
|         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() | ||||
|     { | ||||
|         await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem where parentIndexId not in ( select indexId from ProfileItem )"); | ||||
| 
 | ||||
|         var list = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync(); | ||||
|         _items = new ConcurrentDictionary<string, ProfileGroupItem>(list.Where(t => !string.IsNullOrEmpty(t.ParentIndexId)).ToDictionary(t => t.ParentIndexId!)); | ||||
|     } | ||||
| 
 | ||||
|     private ProfileGroupItem AddProfileGroupItem(string indexId) | ||||
|     { | ||||
|         var profileGroupItem = new ProfileGroupItem() | ||||
|         { | ||||
|             ParentIndexId = indexId, | ||||
|             ChildItems = string.Empty, | ||||
|             MultipleLoad = EMultipleLoad.LeastPing | ||||
|         }; | ||||
| 
 | ||||
|         _items[indexId] = profileGroupItem; | ||||
|         return profileGroupItem; | ||||
|     } | ||||
| 
 | ||||
|     private ProfileGroupItem GetProfileGroupItem(string indexId) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(indexId)) | ||||
|         { | ||||
|             indexId = Utils.GetGuid(false); | ||||
|         } | ||||
| 
 | ||||
|         return _items.GetOrAdd(indexId, AddProfileGroupItem); | ||||
|     } | ||||
| 
 | ||||
|     public async Task ClearAll() | ||||
|     { | ||||
|         await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem "); | ||||
|         _items.Clear(); | ||||
|     } | ||||
| 
 | ||||
|     public async Task SaveTo() | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             var lstExists = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync(); | ||||
|             var existsMap = lstExists.Where(t => !string.IsNullOrEmpty(t.ParentIndexId)).ToDictionary(t => t.ParentIndexId!); | ||||
| 
 | ||||
|             var lstInserts = new List<ProfileGroupItem>(); | ||||
|             var lstUpdates = new List<ProfileGroupItem>(); | ||||
| 
 | ||||
|             foreach (var item in _items.Values) | ||||
|             { | ||||
|                 if (string.IsNullOrEmpty(item.ParentIndexId)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 if (existsMap.ContainsKey(item.ParentIndexId)) | ||||
|                 { | ||||
|                     lstUpdates.Add(item); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     lstInserts.Add(item); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 if (lstInserts.Count > 0) | ||||
|                 { | ||||
|                     await SQLiteHelper.Instance.InsertAllAsync(lstInserts); | ||||
|                 } | ||||
| 
 | ||||
|                 if (lstUpdates.Count > 0) | ||||
|                 { | ||||
|                     await SQLiteHelper.Instance.UpdateAllAsync(lstUpdates); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 Logging.SaveLog(_tag, ex); | ||||
|             } | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Logging.SaveLog(_tag, ex); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public ProfileGroupItem GetOrCreateAndMarkDirty(string indexId) | ||||
|     { | ||||
|         return GetProfileGroupItem(indexId); | ||||
|     } | ||||
| 
 | ||||
|     public async ValueTask DisposeAsync() | ||||
|     { | ||||
|         await SaveTo(); | ||||
|     } | ||||
| 
 | ||||
|     public async Task SaveItemAsync(ProfileGroupItem item) | ||||
|     { | ||||
|         if (item is null) | ||||
|         { | ||||
|             throw new ArgumentNullException(nameof(item)); | ||||
|         } | ||||
| 
 | ||||
|         if (string.IsNullOrWhiteSpace(item.ParentIndexId)) | ||||
|         { | ||||
|             throw new ArgumentException("ParentIndexId required", nameof(item)); | ||||
|         } | ||||
| 
 | ||||
|         _items[item.ParentIndexId] = item; | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             var lst = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().Where(t => t.ParentIndexId == item.ParentIndexId).ToListAsync(); | ||||
|             if (lst != null && lst.Count > 0) | ||||
|             { | ||||
|                 await SQLiteHelper.Instance.UpdateAllAsync(new List<ProfileGroupItem> { item }); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await SQLiteHelper.Instance.InsertAllAsync(new List<ProfileGroupItem> { item }); | ||||
|             } | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Logging.SaveLog(_tag, ex); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -35,6 +35,7 @@ public class TaskManager | |||
| 
 | ||||
|                 await ConfigHandler.SaveConfig(_config); | ||||
|                 await ProfileExManager.Instance.SaveTo(); | ||||
|                 await ProfileGroupItemManager.Instance.SaveTo(); | ||||
|             } | ||||
| 
 | ||||
|             //Execute once 1 hour | ||||
|  |  | |||
							
								
								
									
										13
									
								
								v2rayN/ServiceLib/Models/ProfileGroupItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								v2rayN/ServiceLib/Models/ProfileGroupItem.cs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| using SQLite; | ||||
| namespace ServiceLib.Models; | ||||
| 
 | ||||
| [Serializable] | ||||
| public class ProfileGroupItem | ||||
| { | ||||
|     [PrimaryKey] | ||||
|     public string ParentIndexId { get; set; } | ||||
| 
 | ||||
|     public string ChildItems { get; set; } | ||||
| 
 | ||||
|     public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing; | ||||
| } | ||||
|  | @ -32,18 +32,21 @@ public class ProfileItem : ReactiveObject | |||
|     public string GetSummary() | ||||
|     { | ||||
|         var summary = $"[{(ConfigType).ToString()}] "; | ||||
|         var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.'); | ||||
|         var addr = arrAddr.Length switch | ||||
|         if (IsComplex()) | ||||
|         { | ||||
|             > 2 => $"{arrAddr.First()}***{arrAddr.Last()}", | ||||
|             > 1 => $"***{arrAddr.Last()}", | ||||
|             _ => Address | ||||
|         }; | ||||
|         summary += ConfigType switch | ||||
|             summary += $"[{CoreType.ToString()}]{Remarks}"; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             EConfigType.Custom => $"[{CoreType.ToString()}]{Remarks}", | ||||
|             _ => $"{Remarks}({addr}:{Port})" | ||||
|         }; | ||||
|             var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.'); | ||||
|             var addr = arrAddr.Length switch | ||||
|             { | ||||
|                 > 2 => $"{arrAddr.First()}***{arrAddr.Last()}", | ||||
|                 > 1 => $"***{arrAddr.Last()}", | ||||
|                 _ => Address | ||||
|             }; | ||||
|             summary += $"{Remarks}({addr}:{Port})"; | ||||
|         } | ||||
|         return summary; | ||||
|     } | ||||
| 
 | ||||
|  | @ -61,6 +64,51 @@ public class ProfileItem : ReactiveObject | |||
|         return Network.TrimEx(); | ||||
|     } | ||||
| 
 | ||||
|     public bool IsComplex() | ||||
|     { | ||||
|         return ConfigType is EConfigType.Custom or > EConfigType.Group; | ||||
|     } | ||||
| 
 | ||||
|     public bool IsValid() | ||||
|     { | ||||
|         if (IsComplex()) | ||||
|             return true; | ||||
| 
 | ||||
|         if (Address.IsNullOrEmpty() || Port is <= 0 or >= 65536) | ||||
|             return false; | ||||
| 
 | ||||
|         switch (ConfigType) | ||||
|         { | ||||
|             case EConfigType.VMess: | ||||
|                 if (Id.IsNullOrEmpty() || !Utils.IsGuidByParse(Id)) | ||||
|                     return false; | ||||
|                 break; | ||||
| 
 | ||||
|             case EConfigType.VLESS: | ||||
|                 if (Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(Id) && Id.Length > 30)) | ||||
|                     return false; | ||||
|                 if (!Global.Flows.Contains(Flow)) | ||||
|                     return false; | ||||
|                 break; | ||||
| 
 | ||||
|             case EConfigType.Shadowsocks: | ||||
|                 if (Id.IsNullOrEmpty()) | ||||
|                     return false; | ||||
|                 if (string.IsNullOrEmpty(Security) || !Global.SsSecuritiesInSingbox.Contains(Security)) | ||||
|                     return false; | ||||
|                 break; | ||||
|         } | ||||
| 
 | ||||
|         if ((ConfigType is EConfigType.VLESS or EConfigType.Trojan) | ||||
|             && StreamSecurity == Global.StreamSecurityReality | ||||
|             && PublicKey.IsNullOrEmpty()) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     #endregion function | ||||
| 
 | ||||
|     [PrimaryKey] | ||||
|  |  | |||
|  | @ -145,6 +145,7 @@ public class Outbound4Sbox : BaseServer4Sbox | |||
|     public string? plugin_opts { get; set; } | ||||
|     public List<string>? outbounds { get; set; } | ||||
|     public bool? interrupt_exist_connections { get; set; } | ||||
|     public int? tolerance { get; set; } | ||||
| } | ||||
| 
 | ||||
| public class Endpoints4Sbox : BaseServer4Sbox | ||||
|  |  | |||
							
								
								
									
										252
									
								
								v2rayN/ServiceLib/Resx/ResUI.Designer.cs
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										252
									
								
								v2rayN/ServiceLib/Resx/ResUI.Designer.cs
									
									
									
										generated
									
									
									
								
							|  | @ -672,6 +672,15 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Add Child Configuration 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuAddChildServer { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuAddChildServer", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Add a custom configuration Configuration 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -699,6 +708,24 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Add Policy Group Configuration 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuAddPolicyGroupServer { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuAddPolicyGroupServer", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Add Proxy Chain Configuration 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuAddProxyChainServer { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuAddProxyChainServer", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Import Share Links from clipboard (Ctrl+V) 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -951,6 +978,78 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Generate Policy Group from Multiple Profiles 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuGenGroupMultipleServer { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuGenGroupMultipleServer", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration Fallback by sing-box 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuGenGroupMultipleServerSingBoxFallback { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxFallback", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuGenGroupMultipleServerSingBoxLeastPing { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxLeastPing", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration Fallback by Xray 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuGenGroupMultipleServerXrayFallback { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuGenGroupMultipleServerXrayFallback", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuGenGroupMultipleServerXrayLeastLoad { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastLoad", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuGenGroupMultipleServerXrayLeastPing { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastPing", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration Random by Xray 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuGenGroupMultipleServerXrayRandom { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuGenGroupMultipleServerXrayRandom", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuGenGroupMultipleServerXrayRoundRobin { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuGenGroupMultipleServerXrayRoundRobin", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Global Hotkey Setting 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -1320,6 +1419,15 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Remove Child Configuration 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuRemoveChildServer { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuRemoveChildServer", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Remove duplicate Configurations 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -1473,6 +1581,15 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Server List 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuServerList { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuServerList", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Configurations 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -1482,60 +1599,6 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration to custom configuration 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuSetDefaultMultipleServer { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuSetDefaultMultipleServer", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuSetDefaultMultipleServerSingBoxLeastPing { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuSetDefaultMultipleServerSingBoxLeastPing", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuSetDefaultMultipleServerXrayLeastLoad { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastLoad", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuSetDefaultMultipleServerXrayLeastPing { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastPing", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration Random by Xray 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuSetDefaultMultipleServerXrayRandom { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRandom", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string menuSetDefaultMultipleServerXrayRoundRobin { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRoundRobin", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Set as active Configuration (Enter) 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -1995,6 +2058,15 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Please Add At Least One Configuration 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string PleaseAddAtLeastOneServer { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("PleaseAddAtLeastOneServer", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Please fill Remarks 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -2373,6 +2445,24 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Policy Group 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string TbConfigTypePolicyGroup { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("TbConfigTypePolicyGroup", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Proxy Chain 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string TbConfigTypeProxyChain { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("TbConfigTypeProxyChain", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Confirm 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -2526,6 +2616,15 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Fallback 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string TbFallback { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("TbFallback", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Fingerprint 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -2643,6 +2742,24 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Most Stable 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string TbLeastLoad { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("TbLeastLoad", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Lowest Latency 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string TbLeastPing { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("TbLeastPing", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Address (IPv4, IPv6) 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -2697,6 +2814,15 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Policy Group Type 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string TbPolicyGroupType { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("TbPolicyGroupType", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Port 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -2769,6 +2895,15 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Random 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string TbRandom { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("TbRandom", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 v2ray Full Config Template 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  | @ -2832,6 +2967,15 @@ namespace ServiceLib.Resx { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 Round Robin 的本地化字符串。 | ||||
|         /// </summary> | ||||
|         public static string TbRoundRobin { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("TbRoundRobin", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 | ||||
|         /// </summary> | ||||
|  |  | |||
|  | @ -1377,22 +1377,22 @@ | |||
|   <data name="TbPorts7Tips" xml:space="preserve"> | ||||
|     <value>مخفی و پورت می شود، با کاما (،) جدا می شود</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServer" xml:space="preserve"> | ||||
|     <value>چند سرور به پیکربندی سفارشی</value> | ||||
|   <data name="menuGenGroupMultipleServer" xml:space="preserve"> | ||||
|     <value>Generate Policy Group from Multiple Profiles</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve"> | ||||
|     <value>چند سرور تصادفی توسط Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|     <value>چند سرور RoundRobin توسط Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|     <value>چند سرور LeastPing توسط Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|     <value>چند سرور LeastLoad توسط Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|     <value>LeastPing چند سرور توسط sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuExportConfig" xml:space="preserve"> | ||||
|  | @ -1515,4 +1515,52 @@ | |||
|   <data name="TbFakeIPTips" xml:space="preserve"> | ||||
|     <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> | ||||
|   </data> | ||||
|   <data name="PleaseAddAtLeastOneServer" xml:space="preserve"> | ||||
|     <value>Please Add At Least One Configuration</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypePolicyGroup" xml:space="preserve"> | ||||
|     <value>Policy Group</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypeProxyChain" xml:space="preserve"> | ||||
|     <value>Proxy Chain</value> | ||||
|   </data> | ||||
|   <data name="TbLeastPing" xml:space="preserve"> | ||||
|     <value>Lowest Latency</value> | ||||
|   </data> | ||||
|   <data name="TbRandom" xml:space="preserve"> | ||||
|     <value>Random</value> | ||||
|   </data> | ||||
|   <data name="TbRoundRobin" xml:space="preserve"> | ||||
|     <value>Round Robin</value> | ||||
|   </data> | ||||
|   <data name="TbLeastLoad" xml:space="preserve"> | ||||
|     <value>Most Stable</value> | ||||
|   </data> | ||||
|   <data name="TbPolicyGroupType" xml:space="preserve"> | ||||
|     <value>Policy Group Type</value> | ||||
|   </data> | ||||
|   <data name="menuAddPolicyGroupServer" xml:space="preserve"> | ||||
|     <value>Add Policy Group Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddProxyChainServer" xml:space="preserve"> | ||||
|     <value>Add Proxy Chain Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddChildServer" xml:space="preserve"> | ||||
|     <value>Add Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuRemoveChildServer" xml:space="preserve"> | ||||
|     <value>Remove Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuServerList" xml:space="preserve"> | ||||
|     <value>Server List</value> | ||||
|   </data> | ||||
|   <data name="TbFallback" xml:space="preserve"> | ||||
|     <value>Fallback</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by Xray</value> | ||||
|   </data> | ||||
| </root> | ||||
|  | @ -1377,22 +1377,22 @@ | |||
|   <data name="TbPorts7Tips" xml:space="preserve"> | ||||
|     <value>A portot lefedi, vesszővel (,) elválasztva</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServer" xml:space="preserve"> | ||||
|     <value>Több konfiguráció egyéni konfigurációra</value> | ||||
|   <data name="menuGenGroupMultipleServer" xml:space="preserve"> | ||||
|     <value>Generate Policy Group from Multiple Profiles</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve"> | ||||
|     <value>Több konfiguráció véletlenszerűen Xray szerint</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|     <value>Több konfiguráció RoundRobin Xray szerint</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|     <value>Több konfiguráció legkisebb pinggel Xray szerint</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|     <value>Több konfiguráció legkisebb terheléssel Xray szerint</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|     <value>Több konfiguráció legkisebb pinggel sing-box szerint</value> | ||||
|   </data> | ||||
|   <data name="menuExportConfig" xml:space="preserve"> | ||||
|  | @ -1515,4 +1515,52 @@ | |||
|   <data name="TbFakeIPTips" xml:space="preserve"> | ||||
|     <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> | ||||
|   </data> | ||||
|   <data name="PleaseAddAtLeastOneServer" xml:space="preserve"> | ||||
|     <value>Please Add At Least One Configuration</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypePolicyGroup" xml:space="preserve"> | ||||
|     <value>Policy Group</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypeProxyChain" xml:space="preserve"> | ||||
|     <value>Proxy Chain</value> | ||||
|   </data> | ||||
|   <data name="TbLeastPing" xml:space="preserve"> | ||||
|     <value>Lowest Latency</value> | ||||
|   </data> | ||||
|   <data name="TbRandom" xml:space="preserve"> | ||||
|     <value>Random</value> | ||||
|   </data> | ||||
|   <data name="TbRoundRobin" xml:space="preserve"> | ||||
|     <value>Round Robin</value> | ||||
|   </data> | ||||
|   <data name="TbLeastLoad" xml:space="preserve"> | ||||
|     <value>Most Stable</value> | ||||
|   </data> | ||||
|   <data name="TbPolicyGroupType" xml:space="preserve"> | ||||
|     <value>Policy Group Type</value> | ||||
|   </data> | ||||
|   <data name="menuAddPolicyGroupServer" xml:space="preserve"> | ||||
|     <value>Add Policy Group Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddProxyChainServer" xml:space="preserve"> | ||||
|     <value>Add Proxy Chain Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddChildServer" xml:space="preserve"> | ||||
|     <value>Add Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuRemoveChildServer" xml:space="preserve"> | ||||
|     <value>Remove Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuServerList" xml:space="preserve"> | ||||
|     <value>Server List</value> | ||||
|   </data> | ||||
|   <data name="TbFallback" xml:space="preserve"> | ||||
|     <value>Fallback</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by Xray</value> | ||||
|   </data> | ||||
| </root> | ||||
|  | @ -1377,22 +1377,22 @@ | |||
|   <data name="TbPorts7Tips" xml:space="preserve"> | ||||
|     <value>Will cover the port, separate with commas (,)</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServer" xml:space="preserve"> | ||||
|     <value>Multi-Configuration to custom configuration</value> | ||||
|   <data name="menuGenGroupMultipleServer" xml:space="preserve"> | ||||
|     <value>Generate Policy Group from Multiple Profiles</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Random by Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|     <value>Multi-Configuration RoundRobin by Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|     <value>Multi-Configuration LeastPing by Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|     <value>Multi-Configuration LeastLoad by Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|     <value>Multi-Configuration LeastPing by sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuExportConfig" xml:space="preserve"> | ||||
|  | @ -1515,4 +1515,52 @@ | |||
|   <data name="TbFakeIPTips" xml:space="preserve"> | ||||
|     <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> | ||||
|   </data> | ||||
|   <data name="PleaseAddAtLeastOneServer" xml:space="preserve"> | ||||
|     <value>Please Add At Least One Configuration</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypePolicyGroup" xml:space="preserve"> | ||||
|     <value>Policy Group</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypeProxyChain" xml:space="preserve"> | ||||
|     <value>Proxy Chain</value> | ||||
|   </data> | ||||
|   <data name="TbLeastPing" xml:space="preserve"> | ||||
|     <value>Lowest Latency</value> | ||||
|   </data> | ||||
|   <data name="TbRandom" xml:space="preserve"> | ||||
|     <value>Random</value> | ||||
|   </data> | ||||
|   <data name="TbRoundRobin" xml:space="preserve"> | ||||
|     <value>Round Robin</value> | ||||
|   </data> | ||||
|   <data name="TbLeastLoad" xml:space="preserve"> | ||||
|     <value>Most Stable</value> | ||||
|   </data> | ||||
|   <data name="TbPolicyGroupType" xml:space="preserve"> | ||||
|     <value>Policy Group Type</value> | ||||
|   </data> | ||||
|   <data name="menuAddPolicyGroupServer" xml:space="preserve"> | ||||
|     <value>Add Policy Group Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddProxyChainServer" xml:space="preserve"> | ||||
|     <value>Add Proxy Chain Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddChildServer" xml:space="preserve"> | ||||
|     <value>Add Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuRemoveChildServer" xml:space="preserve"> | ||||
|     <value>Remove Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuServerList" xml:space="preserve"> | ||||
|     <value>Server List</value> | ||||
|   </data> | ||||
|   <data name="TbFallback" xml:space="preserve"> | ||||
|     <value>Fallback</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by Xray</value> | ||||
|   </data> | ||||
| </root> | ||||
|  | @ -1377,22 +1377,22 @@ | |||
|   <data name="TbPorts7Tips" xml:space="preserve"> | ||||
|     <value>Заменит указанный порт, перечисляйте через запятую (,)</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServer" xml:space="preserve"> | ||||
|     <value>От мультиконфигурации к пользовательской конфигурации</value> | ||||
|   <data name="menuGenGroupMultipleServer" xml:space="preserve"> | ||||
|     <value>Generate Policy Group from Multiple Profiles</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve"> | ||||
|     <value>Случайный (Xray)</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|     <value>Круговой (Xray)</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|     <value>Минимальное RTT (минимальное время туда-обратно) (Xray)</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|     <value>Минимальная нагрузка (Xray)</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|     <value>Минимальное RTT (минимальное время туда-обратно) (sing-box)</value> | ||||
|   </data> | ||||
|   <data name="menuExportConfig" xml:space="preserve"> | ||||
|  | @ -1515,4 +1515,52 @@ | |||
|   <data name="TbFakeIPTips" xml:space="preserve"> | ||||
|     <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> | ||||
|   </data> | ||||
|   <data name="PleaseAddAtLeastOneServer" xml:space="preserve"> | ||||
|     <value>Please Add At Least One Configuration</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypePolicyGroup" xml:space="preserve"> | ||||
|     <value>Policy Group</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypeProxyChain" xml:space="preserve"> | ||||
|     <value>Proxy Chain</value> | ||||
|   </data> | ||||
|   <data name="TbLeastPing" xml:space="preserve"> | ||||
|     <value>Lowest Latency</value> | ||||
|   </data> | ||||
|   <data name="TbRandom" xml:space="preserve"> | ||||
|     <value>Random</value> | ||||
|   </data> | ||||
|   <data name="TbRoundRobin" xml:space="preserve"> | ||||
|     <value>Round Robin</value> | ||||
|   </data> | ||||
|   <data name="TbLeastLoad" xml:space="preserve"> | ||||
|     <value>Most Stable</value> | ||||
|   </data> | ||||
|   <data name="TbPolicyGroupType" xml:space="preserve"> | ||||
|     <value>Policy Group Type</value> | ||||
|   </data> | ||||
|   <data name="menuAddPolicyGroupServer" xml:space="preserve"> | ||||
|     <value>Add Policy Group Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddProxyChainServer" xml:space="preserve"> | ||||
|     <value>Add Proxy Chain Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddChildServer" xml:space="preserve"> | ||||
|     <value>Add Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuRemoveChildServer" xml:space="preserve"> | ||||
|     <value>Remove Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuServerList" xml:space="preserve"> | ||||
|     <value>Server List</value> | ||||
|   </data> | ||||
|   <data name="TbFallback" xml:space="preserve"> | ||||
|     <value>Fallback</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by Xray</value> | ||||
|   </data> | ||||
| </root> | ||||
|  | @ -1374,22 +1374,22 @@ | |||
|   <data name="TbPorts7Tips" xml:space="preserve"> | ||||
|     <value>会覆盖端口,多组时用逗号 (,) 隔开</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServer" xml:space="preserve"> | ||||
|     <value>多配置文件产生自定义配置 (多选)</value> | ||||
|   <data name="menuGenGroupMultipleServer" xml:space="preserve"> | ||||
|     <value>多配置文件生成策略组</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve"> | ||||
|     <value>多配置文件随机 Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|     <value>多配置文件负载均衡 Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|     <value>多配置文件最低延迟 Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|     <value>多配置文件最稳定 Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|     <value>多配置文件最低延迟 sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuExportConfig" xml:space="preserve"> | ||||
|  | @ -1512,4 +1512,52 @@ | |||
|   <data name="TbFakeIPTips" xml:space="preserve"> | ||||
|     <value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value> | ||||
|   </data> | ||||
|   <data name="PleaseAddAtLeastOneServer" xml:space="preserve"> | ||||
|     <value>请至少添加一个配置文件</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypePolicyGroup" xml:space="preserve"> | ||||
|     <value>策略组</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypeProxyChain" xml:space="preserve"> | ||||
|     <value>链式代理</value> | ||||
|   </data> | ||||
|   <data name="TbLeastPing" xml:space="preserve"> | ||||
|     <value>最低延迟</value> | ||||
|   </data> | ||||
|   <data name="TbRandom" xml:space="preserve"> | ||||
|     <value>随机</value> | ||||
|   </data> | ||||
|   <data name="TbRoundRobin" xml:space="preserve"> | ||||
|     <value>负载均衡</value> | ||||
|   </data> | ||||
|   <data name="TbLeastLoad" xml:space="preserve"> | ||||
|     <value>最稳定</value> | ||||
|   </data> | ||||
|   <data name="TbPolicyGroupType" xml:space="preserve"> | ||||
|     <value>策略组类型</value> | ||||
|   </data> | ||||
|   <data name="menuAddPolicyGroupServer" xml:space="preserve"> | ||||
|     <value>添加策略组配置文件</value> | ||||
|   </data> | ||||
|   <data name="menuAddProxyChainServer" xml:space="preserve"> | ||||
|     <value>添加链式代理配置文件</value> | ||||
|   </data> | ||||
|   <data name="menuAddChildServer" xml:space="preserve"> | ||||
|     <value>添加子配置文件</value> | ||||
|   </data> | ||||
|   <data name="menuRemoveChildServer" xml:space="preserve"> | ||||
|     <value>删除子配置文件</value> | ||||
|   </data> | ||||
|   <data name="menuServerList" xml:space="preserve"> | ||||
|     <value>服务器列表</value> | ||||
|   </data> | ||||
|   <data name="TbFallback" xml:space="preserve"> | ||||
|     <value>故障转移</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve"> | ||||
|     <value>多配置文件故障转移 sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> | ||||
|     <value>多配置文件故障转移 Xray</value> | ||||
|   </data> | ||||
| </root> | ||||
|  | @ -1374,22 +1374,22 @@ | |||
|   <data name="TbPorts7Tips" xml:space="preserve"> | ||||
|     <value>會覆蓋埠,多組時用逗號 (,) 隔開</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServer" xml:space="preserve"> | ||||
|     <value>多設定檔產生自訂配置 (多選)</value> | ||||
|   <data name="menuGenGroupMultipleServer" xml:space="preserve"> | ||||
|     <value>Generate Policy Group from Multiple Profiles</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve"> | ||||
|     <value>多設定檔隨機 Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve"> | ||||
|     <value>多設定檔負載平衡 Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve"> | ||||
|     <value>多設定檔最低延遲 Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve"> | ||||
|     <value>多設定檔最穩定 Xray</value> | ||||
|   </data> | ||||
|   <data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve"> | ||||
|     <value>多設定檔最低延遲 sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuExportConfig" xml:space="preserve"> | ||||
|  | @ -1512,4 +1512,52 @@ | |||
|   <data name="TbFakeIPTips" xml:space="preserve"> | ||||
|     <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> | ||||
|   </data> | ||||
|   <data name="PleaseAddAtLeastOneServer" xml:space="preserve"> | ||||
|     <value>Please Add At Least One Configuration</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypePolicyGroup" xml:space="preserve"> | ||||
|     <value>Policy Group</value> | ||||
|   </data> | ||||
|   <data name="TbConfigTypeProxyChain" xml:space="preserve"> | ||||
|     <value>Proxy Chain</value> | ||||
|   </data> | ||||
|   <data name="TbLeastPing" xml:space="preserve"> | ||||
|     <value>Lowest Latency</value> | ||||
|   </data> | ||||
|   <data name="TbRandom" xml:space="preserve"> | ||||
|     <value>Random</value> | ||||
|   </data> | ||||
|   <data name="TbRoundRobin" xml:space="preserve"> | ||||
|     <value>Round Robin</value> | ||||
|   </data> | ||||
|   <data name="TbLeastLoad" xml:space="preserve"> | ||||
|     <value>Most Stable</value> | ||||
|   </data> | ||||
|   <data name="TbPolicyGroupType" xml:space="preserve"> | ||||
|     <value>Policy Group Type</value> | ||||
|   </data> | ||||
|   <data name="menuAddPolicyGroupServer" xml:space="preserve"> | ||||
|     <value>Add Policy Group Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddProxyChainServer" xml:space="preserve"> | ||||
|     <value>Add Proxy Chain Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuAddChildServer" xml:space="preserve"> | ||||
|     <value>Add Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuRemoveChildServer" xml:space="preserve"> | ||||
|     <value>Remove Child Configuration</value> | ||||
|   </data> | ||||
|   <data name="menuServerList" xml:space="preserve"> | ||||
|     <value>Server List</value> | ||||
|   </data> | ||||
|   <data name="TbFallback" xml:space="preserve"> | ||||
|     <value>Fallback</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by sing-box</value> | ||||
|   </data> | ||||
|   <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> | ||||
|     <value>Multi-Configuration Fallback by Xray</value> | ||||
|   </data> | ||||
| </root> | ||||
|  | @ -16,7 +16,7 @@ public partial class CoreConfigSingboxService(Config config) | |||
|         try | ||||
|         { | ||||
|             if (node == null | ||||
|                 || node.Port <= 0) | ||||
|                 || !node.IsValid()) | ||||
|             { | ||||
|                 ret.Msg = ResUI.CheckServerSettings; | ||||
|                 return ret; | ||||
|  | @ -28,6 +28,17 @@ public partial class CoreConfigSingboxService(Config config) | |||
|             } | ||||
| 
 | ||||
|             ret.Msg = ResUI.InitialConfiguration; | ||||
|              | ||||
|             if (node?.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) | ||||
|             { | ||||
|                 switch (node.ConfigType) | ||||
|                 { | ||||
|                     case EConfigType.PolicyGroup: | ||||
|                         return await GenerateClientMultipleLoadConfig(node); | ||||
|                     case EConfigType.ProxyChain: | ||||
|                         return await GenerateClientChainConfig(node); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); | ||||
|             if (result.IsNullOrEmpty()) | ||||
|  | @ -142,12 +153,9 @@ public partial class CoreConfigSingboxService(Config config) | |||
|                     continue; | ||||
|                 } | ||||
|                 var item = await AppManager.Instance.GetProfileItem(it.IndexId); | ||||
|                 if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) | ||||
|                 if (item is null || item.IsComplex() || !item.IsValid()) | ||||
|                 { | ||||
|                     if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 //find unused port | ||||
|  | @ -187,27 +195,6 @@ public partial class CoreConfigSingboxService(Config config) | |||
|                 singboxConfig.inbounds.Add(inbound); | ||||
| 
 | ||||
|                 //outbound | ||||
|                 if (item is null) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (item.ConfigType == EConfigType.Shadowsocks | ||||
|                     && !Global.SsSecuritiesInSingbox.Contains(item.Security)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (item.ConfigType == EConfigType.VLESS | ||||
|                  && !Global.Flows.Contains(item.Flow)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan | ||||
|                     && item.StreamSecurity == Global.StreamSecurityReality | ||||
|                     && item.PublicKey.IsNullOrEmpty()) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 var server = await GenServer(item); | ||||
|                 if (server is null) | ||||
|                 { | ||||
|  | @ -266,7 +253,8 @@ public partial class CoreConfigSingboxService(Config config) | |||
|         var ret = new RetResult(); | ||||
|         try | ||||
|         { | ||||
|             if (node is not { Port: > 0 }) | ||||
|             if (node == null | ||||
|                 || !node.IsValid()) | ||||
|             { | ||||
|                 ret.Msg = ResUI.CheckServerSettings; | ||||
|                 return ret; | ||||
|  | @ -344,7 +332,64 @@ public partial class CoreConfigSingboxService(Config config) | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public async Task<RetResult> GenerateClientMultipleLoadConfig(List<ProfileItem> selecteds) | ||||
|     public async Task<RetResult> GenerateClientMultipleLoadConfig(ProfileItem parentNode) | ||||
|     { | ||||
|         var ret = new RetResult(); | ||||
|         try | ||||
|         { | ||||
|             if (_config == null) | ||||
|             { | ||||
|                 ret.Msg = ResUI.CheckServerSettings; | ||||
|                 return ret; | ||||
|             } | ||||
| 
 | ||||
|             ret.Msg = ResUI.InitialConfiguration; | ||||
| 
 | ||||
|             var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); | ||||
|             var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); | ||||
|             if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) | ||||
|             { | ||||
|                 ret.Msg = ResUI.FailedGetDefaultConfiguration; | ||||
|                 return ret; | ||||
|             } | ||||
| 
 | ||||
|             var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result); | ||||
|             if (singboxConfig == null) | ||||
|             { | ||||
|                 ret.Msg = ResUI.FailedGenDefaultConfiguration; | ||||
|                 return ret; | ||||
|             } | ||||
|             singboxConfig.outbounds.RemoveAt(0); | ||||
| 
 | ||||
|             await GenLog(singboxConfig); | ||||
|             await GenInbounds(singboxConfig); | ||||
|             await GenRouting(singboxConfig); | ||||
|             await GenExperimental(singboxConfig); | ||||
| 
 | ||||
|             var groupRet = await GenGroupOutbound(parentNode, singboxConfig); | ||||
|             if (groupRet != 0) | ||||
|             { | ||||
|                 ret.Msg = ResUI.FailedGenDefaultConfiguration; | ||||
|                 return ret; | ||||
|             } | ||||
| 
 | ||||
|             await GenDns(null, singboxConfig); | ||||
|             await ConvertGeo2Ruleset(singboxConfig); | ||||
| 
 | ||||
|             ret.Success = true; | ||||
| 
 | ||||
|             ret.Data = await ApplyFullConfigTemplate(singboxConfig); | ||||
|             return ret; | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Logging.SaveLog(_tag, ex); | ||||
|             ret.Msg = ResUI.FailedGenDefaultConfiguration; | ||||
|             return ret; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public async Task<RetResult> GenerateClientChainConfig(ProfileItem parentNode) | ||||
|     { | ||||
|         var ret = new RetResult(); | ||||
|         try | ||||
|  | @ -378,48 +423,12 @@ public partial class CoreConfigSingboxService(Config config) | |||
|             await GenExperimental(singboxConfig); | ||||
|             singboxConfig.outbounds.RemoveAt(0); | ||||
| 
 | ||||
|             var proxyProfiles = new List<ProfileItem>(); | ||||
|             foreach (var it in selecteds) | ||||
|             { | ||||
|                 if (!Global.SingboxSupportConfigType.Contains(it.ConfigType)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (it.Port <= 0) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 var item = await AppManager.Instance.GetProfileItem(it.IndexId); | ||||
|                 if (item is null) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) | ||||
|                 { | ||||
|                     if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|                 } | ||||
|                 if (item.ConfigType == EConfigType.Shadowsocks | ||||
|                   && !Global.SsSecuritiesInSingbox.Contains(item.Security)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 //outbound | ||||
|                 proxyProfiles.Add(item); | ||||
|             } | ||||
|             if (proxyProfiles.Count <= 0) | ||||
|             var groupRet = await GenGroupOutbound(parentNode, singboxConfig); | ||||
|             if (groupRet != 0) | ||||
|             { | ||||
|                 ret.Msg = ResUI.FailedGenDefaultConfiguration; | ||||
|                 return ret; | ||||
|             } | ||||
|             await GenOutboundsList(proxyProfiles, singboxConfig); | ||||
| 
 | ||||
|             await GenDns(null, singboxConfig); | ||||
|             await ConvertGeo2Ruleset(singboxConfig); | ||||
|  |  | |||
|  | @ -179,13 +179,21 @@ public partial class CoreConfigSingboxService | |||
|             if (node.ConfigType == EConfigType.WireGuard) | ||||
|             { | ||||
|                 var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound); | ||||
|                 await GenEndpoint(node, endpoint); | ||||
|                 var ret = await GenEndpoint(node, endpoint); | ||||
|                 if (ret != 0) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|                 return endpoint; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound); | ||||
|                 await GenOutbound(node, outbound); | ||||
|                 var ret = await GenOutbound(node, outbound); | ||||
|                 if (ret != 0) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|                 return outbound; | ||||
|             } | ||||
|         } | ||||
|  | @ -196,6 +204,67 @@ public partial class CoreConfigSingboxService | |||
|         return await Task.FromResult<BaseServer4Sbox?>(null); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             if (node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain)) | ||||
|             { | ||||
|                 return -1; | ||||
|             } | ||||
|             ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); | ||||
|             if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty()) | ||||
|             { | ||||
|                 return -1; | ||||
|             } | ||||
|             // remove custom nodes | ||||
|             // remove group nodes for proxy chain | ||||
|             // avoid self-reference | ||||
|             var childProfiles = (await Task.WhenAll( | ||||
|                     Utils.String2List(profileGroupItem.ChildItems) | ||||
|                         .Where(p => !p.IsNullOrEmpty()) | ||||
|                         .Select(AppManager.Instance.GetProfileItem) | ||||
|                 )) | ||||
|                 .Where(p => | ||||
|                     p != null | ||||
|                     && p.IsValid() | ||||
|                     && p.ConfigType != EConfigType.Custom | ||||
|                     && (node.ConfigType == EConfigType.PolicyGroup || p.ConfigType < EConfigType.Group) | ||||
|                     && p.IndexId != node.IndexId | ||||
|                 ) | ||||
|                 .ToList(); | ||||
| 
 | ||||
|             if (childProfiles.Count <= 0) | ||||
|             { | ||||
|                 return -1; | ||||
|             } | ||||
|             switch (node.ConfigType) | ||||
|             { | ||||
|                 case EConfigType.PolicyGroup: | ||||
|                     if (ignoreOriginChain) | ||||
|                     { | ||||
|                         await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); | ||||
|                     } | ||||
| 
 | ||||
|                     break; | ||||
|                 case EConfigType.ProxyChain: | ||||
|                     await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Logging.SaveLog(_tag, ex); | ||||
|         } | ||||
|         return await Task.FromResult(0); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenOutboundMux(ProfileItem node, Outbound4Sbox outbound) | ||||
|     { | ||||
|         try | ||||
|  | @ -410,7 +479,7 @@ public partial class CoreConfigSingboxService | |||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig) | ||||
|     private async Task<int> GenOutboundsListWithChain(List<ProfileItem> nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|  | @ -438,6 +507,38 @@ public partial class CoreConfigSingboxService | |||
|             { | ||||
|                 index++; | ||||
| 
 | ||||
|                 if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) | ||||
|                 { | ||||
|                     ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); | ||||
|                     if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty()) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|                     var childProfiles = (await Task.WhenAll( | ||||
|                             Utils.String2List(profileGroupItem.ChildItems) | ||||
|                             .Where(p => !p.IsNullOrEmpty()) | ||||
|                             .Select(AppManager.Instance.GetProfileItem) | ||||
|                         )).Where(p => p != null).ToList(); | ||||
|                     if (childProfiles.Count <= 0) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|                     var childBaseTagName = $"{baseTagName}-{index}"; | ||||
|                     var ret = node.ConfigType switch | ||||
|                     { | ||||
|                         EConfigType.PolicyGroup => | ||||
|                             await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName), | ||||
|                         EConfigType.ProxyChain => | ||||
|                             await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName), | ||||
|                         _ => throw new NotImplementedException() | ||||
|                     }; | ||||
|                     if (ret == 0) | ||||
|                     { | ||||
|                         proxyTags.Add(childBaseTagName); | ||||
|                     } | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 // Handle proxy chain | ||||
|                 string? prevTag = null; | ||||
|                 var currentServer = await GenServer(node); | ||||
|  | @ -450,7 +551,7 @@ public partial class CoreConfigSingboxService | |||
|                 var subItem = await AppManager.Instance.GetSubItem(node.Subid); | ||||
| 
 | ||||
|                 // current proxy | ||||
|                 currentServer.tag = $"{Global.ProxyTag}-{index}"; | ||||
|                 currentServer.tag = $"{baseTagName}-{index}"; | ||||
|                 proxyTags.Add(currentServer.tag); | ||||
| 
 | ||||
|                 if (!node.Subid.IsNullOrEmpty()) | ||||
|  | @ -467,7 +568,7 @@ public partial class CoreConfigSingboxService | |||
|                         { | ||||
|                             var prevOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound); | ||||
|                             await GenOutbound(prevNode, prevOutbound); | ||||
|                             prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; | ||||
|                             prevTag = $"prev-{baseTagName}-{++prevIndex}"; | ||||
|                             prevOutbound.tag = prevTag; | ||||
|                             prevOutbounds.Add(prevOutbound); | ||||
|                         } | ||||
|  | @ -508,16 +609,21 @@ public partial class CoreConfigSingboxService | |||
|                 var outUrltest = new Outbound4Sbox | ||||
|                 { | ||||
|                     type = "urltest", | ||||
|                     tag = $"{Global.ProxyTag}-auto", | ||||
|                     tag = $"{baseTagName}-auto", | ||||
|                     outbounds = proxyTags, | ||||
|                     interrupt_exist_connections = false, | ||||
|                 }; | ||||
| 
 | ||||
|                 if (multipleLoad == EMultipleLoad.Fallback) | ||||
|                 { | ||||
|                     outUrltest.tolerance = 5000; | ||||
|                 } | ||||
| 
 | ||||
|                 // Add selector outbound (manual selection) | ||||
|                 var outSelector = new Outbound4Sbox | ||||
|                 { | ||||
|                     type = "selector", | ||||
|                     tag = Global.ProxyTag, | ||||
|                     tag = baseTagName, | ||||
|                     outbounds = JsonUtils.DeepCopy(proxyTags), | ||||
|                     interrupt_exist_connections = false, | ||||
|                 }; | ||||
|  | @ -574,4 +680,114 @@ public partial class CoreConfigSingboxService | |||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) | ||||
|     { | ||||
|         var resultOutbounds = new List<Outbound4Sbox>(); | ||||
|         var resultEndpoints = new List<Endpoints4Sbox>(); // For endpoints | ||||
|         var proxyTags = new List<string>(); // For selector and urltest outbounds | ||||
|         for (var i = 0; i < nodes.Count; i++) | ||||
|         { | ||||
|             var node = nodes[i]; | ||||
|             var server = await GenServer(node); | ||||
|             if (server is null) | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
|             server.tag = baseTagName + (i + 1).ToString(); | ||||
|             if (server is Endpoints4Sbox endpoint) | ||||
|             { | ||||
|                 resultEndpoints.Add(endpoint); | ||||
|             } | ||||
|             else if (server is Outbound4Sbox outbound) | ||||
|             { | ||||
|                 resultOutbounds.Add(outbound); | ||||
|             } | ||||
|             proxyTags.Add(server.tag); | ||||
|         } | ||||
|         // Add urltest outbound (auto selection based on latency) | ||||
|         if (proxyTags.Count > 0) | ||||
|         { | ||||
|             var outUrltest = new Outbound4Sbox | ||||
|             { | ||||
|                 type = "urltest", | ||||
|                 tag = $"{baseTagName}-auto", | ||||
|                 outbounds = proxyTags, | ||||
|                 interrupt_exist_connections = false, | ||||
|             }; | ||||
|             if (multipleLoad == EMultipleLoad.Fallback) | ||||
|             { | ||||
|                 outUrltest.tolerance = 5000; | ||||
|             } | ||||
|             // Add selector outbound (manual selection) | ||||
|             var outSelector = new Outbound4Sbox | ||||
|             { | ||||
|                 type = "selector", | ||||
|                 tag = baseTagName, | ||||
|                 outbounds = JsonUtils.DeepCopy(proxyTags), | ||||
|                 interrupt_exist_connections = false, | ||||
|             }; | ||||
|             outSelector.outbounds.Insert(0, outUrltest.tag); | ||||
|             // Insert these at the beginning | ||||
|             resultOutbounds.Insert(0, outUrltest); | ||||
|             resultOutbounds.Insert(0, outSelector); | ||||
|         } | ||||
|         singboxConfig.outbounds ??= new(); | ||||
|         resultOutbounds.AddRange(singboxConfig.outbounds); | ||||
|         singboxConfig.outbounds = resultOutbounds; | ||||
|         singboxConfig.endpoints ??= new(); | ||||
|         resultEndpoints.AddRange(singboxConfig.endpoints); | ||||
|         singboxConfig.endpoints = resultEndpoints; | ||||
|         return await Task.FromResult(0); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag) | ||||
|     { | ||||
|         // Based on actual network flow instead of data packets | ||||
|         var nodesReverse = nodes.AsEnumerable().Reverse().ToList(); | ||||
|         var resultOutbounds = new List<Outbound4Sbox>(); | ||||
|         var resultEndpoints = new List<Endpoints4Sbox>(); // For endpoints | ||||
|         for (var i = 0; i < nodesReverse.Count; i++) | ||||
|         { | ||||
|             var node = nodesReverse[i]; | ||||
|             var server = await GenServer(node); | ||||
| 
 | ||||
|             if (server is null) | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             if (i == 0) | ||||
|             { | ||||
|                 server.tag = baseTagName; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 server.tag = baseTagName + i.ToString(); | ||||
|             } | ||||
| 
 | ||||
|             if (i != nodesReverse.Count - 1) | ||||
|             { | ||||
|                 server.detour = baseTagName + (i + 1).ToString(); | ||||
|             } | ||||
| 
 | ||||
|             if (server is Endpoints4Sbox endpoint) | ||||
|             { | ||||
|                 resultEndpoints.Add(endpoint); | ||||
|             } | ||||
|             else if (server is Outbound4Sbox outbound) | ||||
|             { | ||||
|                 resultOutbounds.Add(outbound); | ||||
|             } | ||||
|         } | ||||
|         singboxConfig.outbounds ??= new(); | ||||
|         resultOutbounds.AddRange(singboxConfig.outbounds); | ||||
|         singboxConfig.outbounds = resultOutbounds; | ||||
| 
 | ||||
|         singboxConfig.endpoints ??= new(); | ||||
|         resultEndpoints.AddRange(singboxConfig.endpoints); | ||||
|         singboxConfig.endpoints = resultEndpoints; | ||||
| 
 | ||||
|         return await Task.FromResult(0); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -368,8 +368,10 @@ public partial class CoreConfigSingboxService | |||
|         } | ||||
| 
 | ||||
|         var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); | ||||
| 
 | ||||
|         if (node == null | ||||
|             || !Global.SingboxSupportConfigType.Contains(node.ConfigType)) | ||||
|             || (!Global.SingboxSupportConfigType.Contains(node.ConfigType) | ||||
|             && node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain))) | ||||
|         { | ||||
|             return Global.ProxyTag; | ||||
|         } | ||||
|  | @ -381,13 +383,24 @@ public partial class CoreConfigSingboxService | |||
|             return tag; | ||||
|         } | ||||
| 
 | ||||
|         if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) | ||||
|         { | ||||
|             var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}"; | ||||
|             var ret = await GenGroupOutbound(node, singboxConfig, childBaseTagName); | ||||
|             if (ret == 0) | ||||
|             { | ||||
|                 return childBaseTagName; | ||||
|             } | ||||
|             return Global.ProxyTag; | ||||
|         } | ||||
| 
 | ||||
|         var server = await GenServer(node); | ||||
|         if (server is null) | ||||
|         { | ||||
|             return Global.ProxyTag; | ||||
|         } | ||||
| 
 | ||||
|         server.tag = Global.ProxyTag + node.IndexId.ToString(); | ||||
|         server.tag = tag; | ||||
|         if (server is Endpoints4Sbox endpoint) | ||||
|         { | ||||
|             singboxConfig.endpoints ??= new(); | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ public partial class CoreConfigV2rayService(Config config) | |||
|         try | ||||
|         { | ||||
|             if (node == null | ||||
|                 || node.Port <= 0) | ||||
|                 || !node.IsValid()) | ||||
|             { | ||||
|                 ret.Msg = ResUI.CheckServerSettings; | ||||
|                 return ret; | ||||
|  | @ -30,6 +30,17 @@ public partial class CoreConfigV2rayService(Config config) | |||
| 
 | ||||
|             ret.Msg = ResUI.InitialConfiguration; | ||||
| 
 | ||||
|             if (node?.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) | ||||
|             { | ||||
|                 switch (node.ConfigType) | ||||
|                 { | ||||
|                     case EConfigType.PolicyGroup: | ||||
|                         return await GenerateClientMultipleLoadConfig(node); | ||||
|                     case EConfigType.ProxyChain: | ||||
|                         return await GenerateClientChainConfig(node); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); | ||||
|             if (result.IsNullOrEmpty()) | ||||
|             { | ||||
|  | @ -71,7 +82,112 @@ public partial class CoreConfigV2rayService(Config config) | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public async Task<RetResult> GenerateClientMultipleLoadConfig(List<ProfileItem> selecteds, EMultipleLoad multipleLoad) | ||||
|     public async Task<RetResult> GenerateClientMultipleLoadConfig(ProfileItem parentNode) | ||||
|     { | ||||
|         var ret = new RetResult(); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             if (_config == null) | ||||
|             { | ||||
|                 ret.Msg = ResUI.CheckServerSettings; | ||||
|                 return ret; | ||||
|             } | ||||
| 
 | ||||
|             ret.Msg = ResUI.InitialConfiguration; | ||||
| 
 | ||||
|             string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); | ||||
|             string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); | ||||
|             if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) | ||||
|             { | ||||
|                 ret.Msg = ResUI.FailedGetDefaultConfiguration; | ||||
|                 return ret; | ||||
|             } | ||||
| 
 | ||||
|             var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result); | ||||
|             if (v2rayConfig == null) | ||||
|             { | ||||
|                 ret.Msg = ResUI.FailedGenDefaultConfiguration; | ||||
|                 return ret; | ||||
|             } | ||||
|             v2rayConfig.outbounds.RemoveAt(0); | ||||
| 
 | ||||
|             await GenLog(v2rayConfig); | ||||
|             await GenInbounds(v2rayConfig); | ||||
|             await GenRouting(v2rayConfig); | ||||
|             await GenDns(null, v2rayConfig); | ||||
|             await GenStatistic(v2rayConfig); | ||||
| 
 | ||||
|             var groupRet = await GenGroupOutbound(parentNode, v2rayConfig); | ||||
|             if (groupRet != 0) | ||||
|             { | ||||
|                 ret.Msg = ResUI.FailedGenDefaultConfiguration; | ||||
|                 return ret; | ||||
|             } | ||||
| 
 | ||||
|             var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}"; | ||||
| 
 | ||||
|             //add rule | ||||
|             var rules = v2rayConfig.routing.rules; | ||||
|             if (rules?.Count > 0) | ||||
|             { | ||||
|                 var balancerTagSet = v2rayConfig.routing.balancers | ||||
|                     .Select(b => b.tag) | ||||
|                     .ToHashSet(); | ||||
| 
 | ||||
|                 foreach (var rule in rules) | ||||
|                 { | ||||
|                     if (rule.outboundTag == null) | ||||
|                         continue; | ||||
| 
 | ||||
|                     if (balancerTagSet.Contains(rule.outboundTag)) | ||||
|                     { | ||||
|                         rule.balancerTag = rule.outboundTag; | ||||
|                         rule.outboundTag = null; | ||||
|                         continue; | ||||
|                     } | ||||
| 
 | ||||
|                     var outboundWithSuffix = rule.outboundTag + Global.BalancerTagSuffix; | ||||
|                     if (balancerTagSet.Contains(outboundWithSuffix)) | ||||
|                     { | ||||
|                         rule.balancerTag = outboundWithSuffix; | ||||
|                         rule.outboundTag = null; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) | ||||
|             { | ||||
|                 v2rayConfig.routing.rules.Add(new() | ||||
|                 { | ||||
|                     ip = ["0.0.0.0/0", "::/0"], | ||||
|                     balancerTag = defaultBalancerTag, | ||||
|                     type = "field" | ||||
|                 }); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 v2rayConfig.routing.rules.Add(new() | ||||
|                 { | ||||
|                     network = "tcp,udp", | ||||
|                     balancerTag = defaultBalancerTag, | ||||
|                     type = "field" | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             ret.Success = true; | ||||
| 
 | ||||
|             ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true); | ||||
|             return ret; | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Logging.SaveLog(_tag, ex); | ||||
|             ret.Msg = ResUI.FailedGenDefaultConfiguration; | ||||
|             return ret; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public async Task<RetResult> GenerateClientChainConfig(ProfileItem parentNode) | ||||
|     { | ||||
|         var ret = new RetResult(); | ||||
| 
 | ||||
|  | @ -107,82 +223,12 @@ public partial class CoreConfigV2rayService(Config config) | |||
|             await GenStatistic(v2rayConfig); | ||||
|             v2rayConfig.outbounds.RemoveAt(0); | ||||
| 
 | ||||
|             var proxyProfiles = new List<ProfileItem>(); | ||||
|             foreach (var it in selecteds) | ||||
|             { | ||||
|                 if (!Global.XraySupportConfigType.Contains(it.ConfigType)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (it.Port <= 0) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 var item = await AppManager.Instance.GetProfileItem(it.IndexId); | ||||
|                 if (item is null) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) | ||||
|                 { | ||||
|                     if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|                 } | ||||
|                 if (item.ConfigType == EConfigType.Shadowsocks | ||||
|                   && !Global.SsSecuritiesInSingbox.Contains(item.Security)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 //outbound | ||||
|                 proxyProfiles.Add(item); | ||||
|             } | ||||
|             if (proxyProfiles.Count <= 0) | ||||
|             var groupRet = await GenGroupOutbound(parentNode, v2rayConfig); | ||||
|             if (groupRet != 0) | ||||
|             { | ||||
|                 ret.Msg = ResUI.FailedGenDefaultConfiguration; | ||||
|                 return ret; | ||||
|             } | ||||
|             await GenOutboundsList(proxyProfiles, v2rayConfig); | ||||
| 
 | ||||
|             //add balancers | ||||
|             await GenBalancer(v2rayConfig, multipleLoad); | ||||
| 
 | ||||
|             var balancer = v2rayConfig.routing.balancers.First(); | ||||
| 
 | ||||
|             //add rule | ||||
|             var rules = v2rayConfig.routing.rules.Where(t => t.outboundTag == Global.ProxyTag).ToList(); | ||||
|             if (rules?.Count > 0) | ||||
|             { | ||||
|                 foreach (var rule in rules) | ||||
|                 { | ||||
|                     rule.outboundTag = null; | ||||
|                     rule.balancerTag = balancer.tag; | ||||
|                 } | ||||
|             } | ||||
|             if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) | ||||
|             { | ||||
|                 v2rayConfig.routing.rules.Add(new() | ||||
|                 { | ||||
|                     ip = ["0.0.0.0/0", "::/0"], | ||||
|                     balancerTag = balancer.tag, | ||||
|                     type = "field" | ||||
|                 }); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 v2rayConfig.routing.rules.Add(new() | ||||
|                 { | ||||
|                     network = "tcp,udp", | ||||
|                     balancerTag = balancer.tag, | ||||
|                     type = "field" | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             ret.Success = true; | ||||
| 
 | ||||
|  | @ -255,12 +301,9 @@ public partial class CoreConfigV2rayService(Config config) | |||
|                     continue; | ||||
|                 } | ||||
|                 var item = await AppManager.Instance.GetProfileItem(it.IndexId); | ||||
|                 if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) | ||||
|                 if (item is null || item.IsComplex() || !item.IsValid()) | ||||
|                 { | ||||
|                     if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 //find unused port | ||||
|  | @ -289,28 +332,6 @@ public partial class CoreConfigV2rayService(Config config) | |||
|                 it.Port = port; | ||||
|                 it.AllowTest = true; | ||||
| 
 | ||||
|                 //outbound | ||||
|                 if (item is null) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (item.ConfigType == EConfigType.Shadowsocks | ||||
|                     && !Global.SsSecuritiesInXray.Contains(item.Security)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (item.ConfigType == EConfigType.VLESS | ||||
|                  && !Global.Flows.Contains(item.Flow)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan | ||||
|                     && item.StreamSecurity == Global.StreamSecurityReality | ||||
|                     && item.PublicKey.IsNullOrEmpty()) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 //inbound | ||||
|                 Inbounds4Ray inbound = new() | ||||
|                 { | ||||
|  | @ -321,6 +342,7 @@ public partial class CoreConfigV2rayService(Config config) | |||
|                 inbound.tag = inbound.protocol + inbound.port.ToString(); | ||||
|                 v2rayConfig.inbounds.Add(inbound); | ||||
| 
 | ||||
|                 //outbound | ||||
|                 var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); | ||||
|                 await GenOutbound(item, outbound); | ||||
|                 outbound.tag = Global.ProxyTag + inbound.port.ToString(); | ||||
|  | @ -354,7 +376,8 @@ public partial class CoreConfigV2rayService(Config config) | |||
|         var ret = new RetResult(); | ||||
|         try | ||||
|         { | ||||
|             if (node is not { Port: > 0 }) | ||||
|             if (node == null | ||||
|                 || !node.IsValid()) | ||||
|             { | ||||
|                 ret.Msg = ResUI.CheckServerSettings; | ||||
|                 return ret; | ||||
|  |  | |||
|  | @ -2,24 +2,24 @@ namespace ServiceLib.Services.CoreConfig; | |||
| 
 | ||||
| public partial class CoreConfigV2rayService | ||||
| { | ||||
|     private async Task<int> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad) | ||||
|     private async Task<int> GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) | ||||
|     { | ||||
|         if (multipleLoad == EMultipleLoad.LeastPing) | ||||
|         { | ||||
|             var observatory = new Observatory4Ray | ||||
|             { | ||||
|                 subjectSelector = [Global.ProxyTag], | ||||
|                 subjectSelector = [baseTagName], | ||||
|                 probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, | ||||
|                 probeInterval = "3m", | ||||
|                 enableConcurrency = true, | ||||
|             }; | ||||
|             v2rayConfig.observatory = observatory; | ||||
|         } | ||||
|         else if (multipleLoad == EMultipleLoad.LeastLoad) | ||||
|         else if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback) | ||||
|         { | ||||
|             var burstObservatory = new BurstObservatory4Ray | ||||
|             { | ||||
|                 subjectSelector = [Global.ProxyTag], | ||||
|                 subjectSelector = [baseTagName], | ||||
|                 pingConfig = new() | ||||
|                 { | ||||
|                     destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, | ||||
|  | @ -30,6 +30,11 @@ public partial class CoreConfigV2rayService | |||
|             }; | ||||
|             v2rayConfig.burstObservatory = burstObservatory; | ||||
|         } | ||||
|         return await Task.FromResult(0); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<string> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string selector = Global.ProxyTag) | ||||
|     { | ||||
|         var strategyType = multipleLoad switch | ||||
|         { | ||||
|             EMultipleLoad.Random => "random", | ||||
|  | @ -38,13 +43,22 @@ public partial class CoreConfigV2rayService | |||
|             EMultipleLoad.LeastLoad => "leastLoad", | ||||
|             _ => "roundRobin", | ||||
|         }; | ||||
|         var balancerTag = $"{selector}{Global.BalancerTagSuffix}"; | ||||
|         var balancer = new BalancersItem4Ray | ||||
|         { | ||||
|             selector = [Global.ProxyTag], | ||||
|             strategy = new() { type = strategyType }, | ||||
|             tag = $"{Global.ProxyTag}-round", | ||||
|             selector = [selector], | ||||
|             strategy = new() | ||||
|             { | ||||
|                 type = strategyType, | ||||
|                 settings = new() | ||||
|                 { | ||||
|                     expected = 1, | ||||
|                 }, | ||||
|             }, | ||||
|             tag = balancerTag, | ||||
|         }; | ||||
|         v2rayConfig.routing.balancers = [balancer]; | ||||
|         return await Task.FromResult(0); | ||||
|         v2rayConfig.routing.balancers ??= new(); | ||||
|         v2rayConfig.routing.balancers.Add(balancer); | ||||
|         return await Task.FromResult(balancerTag); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -480,6 +480,69 @@ public partial class CoreConfigV2rayService | |||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenGroupOutbound(ProfileItem node, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             if (node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain)) | ||||
|             { | ||||
|                 return -1; | ||||
|             } | ||||
|             ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); | ||||
|             if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty()) | ||||
|             { | ||||
|                 return -1; | ||||
|             } | ||||
|             // remove custom nodes | ||||
|             // remove group nodes for proxy chain | ||||
|             var childProfiles = (await Task.WhenAll( | ||||
|                     Utils.String2List(profileGroupItem.ChildItems) | ||||
|                         .Where(p => !p.IsNullOrEmpty()) | ||||
|                         .Select(AppManager.Instance.GetProfileItem) | ||||
|                 )) | ||||
|                 .Where(p => | ||||
|                     p != null && | ||||
|                     p.IsValid() && | ||||
|                     p.ConfigType != EConfigType.Custom && | ||||
|                     (node.ConfigType == EConfigType.PolicyGroup || p.ConfigType < EConfigType.Group) | ||||
|                 ) | ||||
|                 .ToList(); | ||||
| 
 | ||||
|             if (childProfiles.Count <= 0) | ||||
|             { | ||||
|                 return -1; | ||||
|             } | ||||
|             switch (node.ConfigType) | ||||
|             { | ||||
|                 case EConfigType.PolicyGroup: | ||||
|                     if (ignoreOriginChain) | ||||
|                     { | ||||
|                         await GenOutboundsList(childProfiles, v2rayConfig, baseTagName); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         await GenOutboundsListWithChain(childProfiles, v2rayConfig, baseTagName); | ||||
|                     } | ||||
|                     break; | ||||
|                 case EConfigType.ProxyChain: | ||||
|                     await GenChainOutboundsList(childProfiles, v2rayConfig, baseTagName); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
| 
 | ||||
|             //add balancers | ||||
|             await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); | ||||
|             await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); | ||||
| 
 | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Logging.SaveLog(_tag, ex); | ||||
|         } | ||||
|         return await Task.FromResult(0); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenMoreOutbounds(ProfileItem node, V2rayConfig v2rayConfig) | ||||
|     { | ||||
|         //fragment proxy | ||||
|  | @ -552,7 +615,7 @@ public partial class CoreConfigV2rayService | |||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig) | ||||
|     private async Task<int> GenOutboundsListWithChain(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|  | @ -577,6 +640,34 @@ public partial class CoreConfigV2rayService | |||
|             { | ||||
|                 index++; | ||||
| 
 | ||||
|                 if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) | ||||
|                 { | ||||
|                     ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); | ||||
|                     if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty()) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|                     var childProfiles = (await Task.WhenAll( | ||||
|                             Utils.String2List(profileGroupItem.ChildItems) | ||||
|                             .Where(p => !p.IsNullOrEmpty()) | ||||
|                             .Select(AppManager.Instance.GetProfileItem) | ||||
|                         )).Where(p => p != null).ToList(); | ||||
|                     if (childProfiles.Count <= 0) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|                     var childBaseTagName = $"{baseTagName}-{index}"; | ||||
|                     var ret = node.ConfigType switch | ||||
|                     { | ||||
|                         EConfigType.PolicyGroup => | ||||
|                             await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName), | ||||
|                         EConfigType.ProxyChain => | ||||
|                             await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName), | ||||
|                         _ => throw new NotImplementedException() | ||||
|                     }; | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 // Handle proxy chain | ||||
|                 string? prevTag = null; | ||||
|                 var currentOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); | ||||
|  | @ -590,7 +681,7 @@ public partial class CoreConfigV2rayService | |||
| 
 | ||||
|                 // current proxy | ||||
|                 await GenOutbound(node, currentOutbound); | ||||
|                 currentOutbound.tag = $"{Global.ProxyTag}-{index}"; | ||||
|                 currentOutbound.tag = $"{baseTagName}-{index}"; | ||||
| 
 | ||||
|                 if (!node.Subid.IsNullOrEmpty()) | ||||
|                 { | ||||
|  | @ -606,7 +697,7 @@ public partial class CoreConfigV2rayService | |||
|                         { | ||||
|                             var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); | ||||
|                             await GenOutbound(prevNode, prevOutbound); | ||||
|                             prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; | ||||
|                             prevTag = $"prev-{baseTagName}-{++prevIndex}"; | ||||
|                             prevOutbound.tag = prevTag; | ||||
|                             prevOutbounds.Add(prevOutbound); | ||||
|                         } | ||||
|  | @ -692,4 +783,78 @@ public partial class CoreConfigV2rayService | |||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) | ||||
|     { | ||||
|         var resultOutbounds = new List<Outbounds4Ray>(); | ||||
|         for (var i = 0; i < nodes.Count; i++) | ||||
|         { | ||||
|             var node = nodes[i]; | ||||
|             var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); | ||||
|             if (txtOutbound.IsNullOrEmpty()) | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
|             var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); | ||||
|             var result = await GenOutbound(node, outbound); | ||||
|             if (result != 0) | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
|             outbound.tag = baseTagName + (i + 1).ToString(); | ||||
|             resultOutbounds.Add(outbound); | ||||
|         } | ||||
|         v2rayConfig.outbounds ??= new(); | ||||
|         resultOutbounds.AddRange(v2rayConfig.outbounds); | ||||
|         v2rayConfig.outbounds = resultOutbounds; | ||||
|         return await Task.FromResult(0); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, V2rayConfig v2RayConfig, string baseTagName = Global.ProxyTag) | ||||
|     { | ||||
|         // Based on actual network flow instead of data packets | ||||
|         var nodesReverse = nodes.AsEnumerable().Reverse().ToList(); | ||||
|         var resultOutbounds = new List<Outbounds4Ray>(); | ||||
|         for (var i = 0; i < nodesReverse.Count; i++) | ||||
|         { | ||||
|             var node = nodesReverse[i]; | ||||
|             var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); | ||||
|             if (txtOutbound.IsNullOrEmpty()) | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
|             var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); | ||||
|             var result = await GenOutbound(node, outbound); | ||||
| 
 | ||||
|             if (result != 0) | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             if (i == 0) | ||||
|             { | ||||
|                 outbound.tag = baseTagName; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // avoid v2ray observe | ||||
|                 outbound.tag = "chain-" + baseTagName + i.ToString(); | ||||
|             } | ||||
| 
 | ||||
|             if (i != nodesReverse.Count - 1) | ||||
|             { | ||||
|                 outbound.streamSettings.sockopt = new() | ||||
|                 { | ||||
|                     dialerProxy = "chain-" + baseTagName + (i + 1).ToString() | ||||
|                 }; | ||||
|             } | ||||
| 
 | ||||
|             resultOutbounds.Add(outbound); | ||||
|         } | ||||
|         v2RayConfig.outbounds ??= new(); | ||||
|         resultOutbounds.AddRange(v2RayConfig.outbounds); | ||||
|         v2RayConfig.outbounds = resultOutbounds; | ||||
| 
 | ||||
|         return await Task.FromResult(0); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -125,8 +125,10 @@ public partial class CoreConfigV2rayService | |||
|         } | ||||
| 
 | ||||
|         var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); | ||||
| 
 | ||||
|         if (node == null | ||||
|             || !Global.XraySupportConfigType.Contains(node.ConfigType)) | ||||
|             || (!Global.XraySupportConfigType.Contains(node.ConfigType) | ||||
|             && node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain))) | ||||
|         { | ||||
|             return Global.ProxyTag; | ||||
|         } | ||||
|  | @ -137,6 +139,17 @@ public partial class CoreConfigV2rayService | |||
|             return tag; | ||||
|         } | ||||
| 
 | ||||
|         if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) | ||||
|         { | ||||
|             var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}"; | ||||
|             var ret = await GenGroupOutbound(node, v2rayConfig, childBaseTagName); | ||||
|             if (ret == 0) | ||||
|             { | ||||
|                 return childBaseTagName; | ||||
|             } | ||||
|             return Global.ProxyTag; | ||||
|         } | ||||
| 
 | ||||
|         var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); | ||||
|         var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); | ||||
|         await GenOutbound(node, outbound); | ||||
|  |  | |||
							
								
								
									
										225
									
								
								v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,225 @@ | |||
| using System.Reactive; | ||||
| using DynamicData.Binding; | ||||
| using ReactiveUI; | ||||
| using ReactiveUI.Fody.Helpers; | ||||
| 
 | ||||
| namespace ServiceLib.ViewModels; | ||||
| 
 | ||||
| public class AddGroupServerViewModel : MyReactiveObject | ||||
| { | ||||
|     [Reactive] | ||||
|     public ProfileItem SelectedSource { get; set; } | ||||
| 
 | ||||
|     [Reactive] | ||||
|     public ProfileItem SelectedChild { get; set; } | ||||
| 
 | ||||
|     [Reactive] | ||||
|     public IList<ProfileItem> SelectedChildren { get; set; } | ||||
| 
 | ||||
|     [Reactive] | ||||
|     public string? CoreType { get; set; } | ||||
| 
 | ||||
|     [Reactive] | ||||
|     public string? PolicyGroupType { get; set; } | ||||
| 
 | ||||
|     public IObservableCollection<ProfileItem> ChildItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>(); | ||||
| 
 | ||||
|     //public ReactiveCommand<Unit, Unit> AddCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> RemoveCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> MoveTopCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> MoveUpCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> MoveDownCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> MoveBottomCmd { get; } | ||||
| 
 | ||||
|     public ReactiveCommand<Unit, Unit> SaveCmd { get; } | ||||
| 
 | ||||
|     public AddGroupServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView) | ||||
|     { | ||||
|         _config = AppManager.Instance.Config; | ||||
|         _updateView = updateView; | ||||
| 
 | ||||
|         var canEditRemove = this.WhenAnyValue( | ||||
|             x => x.SelectedChild, | ||||
|             SelectedChild => SelectedChild != null && !SelectedChild.Remarks.IsNullOrEmpty()); | ||||
| 
 | ||||
|         RemoveCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await ChildRemoveAsync(); | ||||
|         }, canEditRemove); | ||||
|         MoveTopCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await MoveServer(EMove.Top); | ||||
|         }, canEditRemove); | ||||
|         MoveUpCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await MoveServer(EMove.Up); | ||||
|         }, canEditRemove); | ||||
|         MoveDownCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await MoveServer(EMove.Down); | ||||
|         }, canEditRemove); | ||||
|         MoveBottomCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await MoveServer(EMove.Bottom); | ||||
|         }, canEditRemove); | ||||
|         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.Fallback => ResUI.TbFallback, | ||||
|             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<string>() : 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; | ||||
|         } | ||||
|         foreach (var it in SelectedChildren ?? [SelectedChild]) | ||||
|         { | ||||
|             if (it != null) | ||||
|             { | ||||
|                 ChildItemsObs.Remove(it); | ||||
|             } | ||||
|         } | ||||
|         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; | ||||
|         } | ||||
|         var selectedChild = JsonUtils.DeepCopy(SelectedChild); | ||||
|         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<string>(); | ||||
|         foreach (var item in ChildItemsObs) | ||||
|         { | ||||
|             if (!item.IndexId.IsNullOrEmpty()) | ||||
|             { | ||||
|                 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.TbFallback => EMultipleLoad.Fallback, | ||||
|             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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -23,6 +23,8 @@ public class MainWindowViewModel : MyReactiveObject | |||
|     public ReactiveCommand<Unit, Unit> AddWireguardServerCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> AddAnytlsServerCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> AddCustomServerCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> AddPolicyGroupServerCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> AddProxyChainServerCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> AddServerViaClipboardCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> AddServerViaScanCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> 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); | ||||
|  | @ -252,6 +262,7 @@ public class MainWindowViewModel : MyReactiveObject | |||
|         await ConfigHandler.InitBuiltinDNS(_config); | ||||
|         await ConfigHandler.InitBuiltinFullConfigTemplate(_config); | ||||
|         await ProfileExManager.Instance.Init(); | ||||
|         await ProfileGroupItemManager.Instance.Init(); | ||||
|         await CoreManager.Instance.Init(_config, UpdateHandler); | ||||
|         TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler); | ||||
| 
 | ||||
|  | @ -340,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); | ||||
|  |  | |||
|  | @ -52,11 +52,13 @@ public class ProfilesViewModel : MyReactiveObject | |||
|     public ReactiveCommand<Unit, Unit> CopyServerCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> SetDefaultServerCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> ShareServerCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayRandomCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayRoundRobinCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayLeastPingCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayLeastLoadCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerSingBoxLeastPingCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayRandomCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayRoundRobinCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayLeastPingCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayLeastLoadCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayFallbackCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> GenGroupMultipleServerSingBoxLeastPingCmd { get; } | ||||
|     public ReactiveCommand<Unit, Unit> GenGroupMultipleServerSingBoxFallbackCmd { get; } | ||||
| 
 | ||||
|     //servers move | ||||
|     public ReactiveCommand<Unit, Unit> MoveTopCmd { get; } | ||||
|  | @ -138,25 +140,33 @@ public class ProfilesViewModel : MyReactiveObject | |||
|         { | ||||
|             await ShareServerAsync(); | ||||
|         }, canEditRemove); | ||||
|         SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         GenGroupMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random); | ||||
|             await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Random); | ||||
|         }, canEditRemove); | ||||
|         SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         GenGroupMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); | ||||
|             await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); | ||||
|         }, canEditRemove); | ||||
|         SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         GenGroupMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); | ||||
|             await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); | ||||
|         }, canEditRemove); | ||||
|         SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         GenGroupMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); | ||||
|             await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); | ||||
|         }, canEditRemove); | ||||
|         SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         GenGroupMultipleServerXrayFallbackCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); | ||||
|             await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Fallback); | ||||
|         }, canEditRemove); | ||||
|         GenGroupMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); | ||||
|         }, canEditRemove); | ||||
|         GenGroupMultipleServerSingBoxFallbackCmd = ReactiveCommand.CreateFromTask(async () => | ||||
|         { | ||||
|             await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.Fallback); | ||||
|         }, canEditRemove); | ||||
| 
 | ||||
|         //servers move | ||||
|  | @ -500,6 +510,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); | ||||
|  | @ -615,7 +629,7 @@ public class ProfilesViewModel : MyReactiveObject | |||
|         await _updateView?.Invoke(EViewAction.ShareServer, url); | ||||
|     } | ||||
| 
 | ||||
|     private async Task SetDefaultMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad) | ||||
|     private async Task GenGroupMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad) | ||||
|     { | ||||
|         var lstSelected = await GetProfileItems(true); | ||||
|         if (lstSelected == null) | ||||
|  | @ -623,7 +637,7 @@ public class ProfilesViewModel : MyReactiveObject | |||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad); | ||||
|         var ret = await ConfigHandler.AddGroupServer4Multiple(_config, lstSelected, coreType, multipleLoad, SelectedSub?.Id); | ||||
|         if (ret.Success != true) | ||||
|         { | ||||
|             NoticeManager.Instance.Enqueue(ResUI.OperationFailed); | ||||
|  |  | |||
							
								
								
									
										151
									
								
								v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,151 @@ | |||
| <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> | ||||
| 
 | ||||
|         <Grid DockPanel.Dock="Top" 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> | ||||
|         </Grid> | ||||
|         <TabControl> | ||||
|             <TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList}"> | ||||
|                 <DataGrid | ||||
|                     x:Name="lstChild" | ||||
|                     Grid.Row="1" | ||||
|                     AutoGenerateColumns="False" | ||||
|                     Background="Transparent" | ||||
|                     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> | ||||
|             </TabItem> | ||||
|         </TabControl> | ||||
|     </DockPanel> | ||||
| </Window> | ||||
							
								
								
									
										166
									
								
								v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,166 @@ | |||
| using System.Reactive.Disposables; | ||||
| using Avalonia.Controls; | ||||
| using Avalonia.Input; | ||||
| using Avalonia.Interactivity; | ||||
| using DynamicData; | ||||
| 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(); | ||||
|         lstChild.SelectionChanged += LstChild_SelectionChanged; | ||||
| 
 | ||||
|         ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler); | ||||
| 
 | ||||
|         cmbCoreType.ItemsSource = Global.CoreTypes; | ||||
|         cmbPolicyGroupType.ItemsSource = new List<string> | ||||
|         { | ||||
|             ResUI.TbLeastPing, | ||||
|             ResUI.TbFallback, | ||||
|             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(); | ||||
|         if (ViewModel?.SelectedSource?.ConfigType == EConfigType.PolicyGroup) | ||||
|         { | ||||
|             selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true); | ||||
|         } | ||||
|         selectWindow.AllowMultiSelect(true); | ||||
|         var result = await selectWindow.ShowDialog<bool?>(this); | ||||
|         if (result == true) | ||||
|         { | ||||
|             var profiles = await selectWindow.ProfileItems; | ||||
|             ViewModel?.ChildItemsObs.AddRange(profiles); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void LstChild_SelectionChanged(object? sender, SelectionChangedEventArgs e) | ||||
|     { | ||||
|         if (ViewModel != null) | ||||
|         { | ||||
|             ViewModel.SelectedChildren = lstChild.SelectedItems.Cast<ProfileItem>().ToList(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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}" /> | ||||
|  |  | |||
|  | @ -83,6 +83,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); | ||||
|  | @ -207,6 +209,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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -99,13 +99,14 @@ | |||
|                         <MenuItem x:Name="menuCopyServer" Header="{x:Static resx:ResUI.menuCopyServer}" /> | ||||
|                         <MenuItem x:Name="menuShareServer" Header="{x:Static resx:ResUI.menuShareServer}" /> | ||||
|                         <Separator /> | ||||
|                         <MenuItem Header="{x:Static resx:ResUI.menuSetDefaultMultipleServer}"> | ||||
|                             <MenuItem x:Name="menuSetDefaultMultipleServerXrayRandom" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayRandom}" /> | ||||
|                             <MenuItem x:Name="menuSetDefaultMultipleServerXrayRoundRobin" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayRoundRobin}" /> | ||||
|                             <MenuItem x:Name="menuSetDefaultMultipleServerXrayLeastPing" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayLeastPing}" /> | ||||
|                             <MenuItem x:Name="menuSetDefaultMultipleServerXrayLeastLoad" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayLeastLoad}" /> | ||||
|                         <MenuItem Header="{x:Static resx:ResUI.menuGenGroupMultipleServer}"> | ||||
|                             <MenuItem x:Name="menuGenGroupMultipleServerXrayRandom" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRandom}" /> | ||||
|                             <MenuItem x:Name="menuGenGroupMultipleServerXrayRoundRobin" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRoundRobin}" /> | ||||
|                             <MenuItem x:Name="menuGenGroupMultipleServerXrayLeastPing" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastPing}" /> | ||||
|                             <MenuItem x:Name="menuGenGroupMultipleServerXrayLeastLoad" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastLoad}" /> | ||||
|                             <Separator /> | ||||
|                             <MenuItem x:Name="menuSetDefaultMultipleServerSingBoxLeastPing" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerSingBoxLeastPing}" /> | ||||
|                             <MenuItem x:Name="menuGenGroupMultipleServerSingBoxLeastPing" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxLeastPing}" /> | ||||
|                             <MenuItem x:Name="menuGenGroupMultipleServerSingBoxFallback" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxFallback}" /> | ||||
|                         </MenuItem> | ||||
|                         <Separator /> | ||||
|                         <MenuItem x:Name="menuMixedTestServer" Header="{x:Static resx:ResUI.menuMixedTestServer}" /> | ||||
|  |  | |||
|  | @ -66,11 +66,12 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel> | |||
|             this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRandomCmd, v => v.menuSetDefaultMultipleServerXrayRandom).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRoundRobinCmd, v => v.menuSetDefaultMultipleServerXrayRoundRobin).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastPingCmd, v => v.menuSetDefaultMultipleServerXrayLeastPing).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastLoadCmd, v => v.menuSetDefaultMultipleServerXrayLeastLoad).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerSingBoxLeastPingCmd, v => v.menuSetDefaultMultipleServerSingBoxLeastPing).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables); | ||||
| 
 | ||||
|             //servers move | ||||
|             //this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); | ||||
|  | @ -167,6 +168,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; | ||||
|  |  | |||
|  | @ -63,7 +63,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel> | |||
|     private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e) | ||||
|     { | ||||
|         var selectWindow = new ProfilesSelectWindow(); | ||||
|         selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true); | ||||
|         selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true); | ||||
|         var result = await selectWindow.ShowDialog<bool?>(this); | ||||
|         if (result == true) | ||||
|         { | ||||
|  | @ -78,7 +78,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel> | |||
|     private async void BtnSelectNextProfile_Click(object? sender, RoutedEventArgs e) | ||||
|     { | ||||
|         var selectWindow = new ProfilesSelectWindow(); | ||||
|         selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true); | ||||
|         selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true); | ||||
|         var result = await selectWindow.ShowDialog<bool?>(this); | ||||
|         if (result == true) | ||||
|         { | ||||
|  |  | |||
							
								
								
									
										213
									
								
								v2rayN/v2rayN/Views/AddGroupServerWindow.xaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								v2rayN/v2rayN/Views/AddGroupServerWindow.xaml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,213 @@ | |||
| <base:WindowBase | ||||
|     x:Class="v2rayN.Views.AddGroupServerWindow" | ||||
|     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||||
|     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||||
|     xmlns:base="clr-namespace:v2rayN.Base" | ||||
|     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | ||||
|     xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" | ||||
|     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | ||||
|     xmlns:reactiveui="http://reactiveui.net" | ||||
|     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:TypeArguments="vms:AddGroupServerViewModel" | ||||
|     ResizeMode="CanResize" | ||||
|     ShowInTaskbar="False" | ||||
|     Style="{StaticResource WindowGlobal}" | ||||
|     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" | ||||
|                 Style="{StaticResource DefButton}" /> | ||||
|             <Button | ||||
|                 x:Name="btnCancel" | ||||
|                 Width="100" | ||||
|                 Margin="{StaticResource MarginLeftRight8}" | ||||
|                 Content="{x:Static resx:ResUI.TbCancel}" | ||||
|                 IsCancel="true" | ||||
|                 Style="{StaticResource DefButton}" /> | ||||
|         </StackPanel> | ||||
|         <Grid DockPanel.Dock="Top"> | ||||
|             <Grid.RowDefinitions> | ||||
|                 <RowDefinition Height="Auto" /> | ||||
|                 <RowDefinition Height="*" /> | ||||
|                 <RowDefinition Height="Auto" /> | ||||
|                 <RowDefinition Height="Auto" /> | ||||
|                 <RowDefinition Height="Auto" /> | ||||
|                 <RowDefinition Height="Auto" /> | ||||
|                 <RowDefinition Height="Auto" /> | ||||
|                 <RowDefinition Height="Auto" /> | ||||
|                 <RowDefinition Height="Auto" /> | ||||
|             </Grid.RowDefinitions> | ||||
| 
 | ||||
|             <Grid Grid.Row="0"> | ||||
|                 <Grid.RowDefinitions> | ||||
|                     <RowDefinition Height="Auto" /> | ||||
|                     <RowDefinition Height="Auto" /> | ||||
|                     <RowDefinition Height="Auto" /> | ||||
|                     <RowDefinition Height="Auto" /> | ||||
|                     <RowDefinition Height="Auto" /> | ||||
|                 </Grid.RowDefinitions> | ||||
|                 <Grid.ColumnDefinitions> | ||||
|                     <ColumnDefinition Width="180" /> | ||||
|                     <ColumnDefinition Width="Auto" /> | ||||
|                     <ColumnDefinition Width="Auto" /> | ||||
|                 </Grid.ColumnDefinitions> | ||||
| 
 | ||||
|                 <TextBlock | ||||
|                     Grid.Row="0" | ||||
|                     Grid.Column="0" | ||||
|                     Margin="{StaticResource Margin4}" | ||||
|                     Style="{StaticResource ModuleTitle}" | ||||
|                     Text="{x:Static resx:ResUI.menuServers}" /> | ||||
| 
 | ||||
|                 <TextBlock | ||||
|                     Grid.Row="1" | ||||
|                     Grid.Column="0" | ||||
|                     Margin="{StaticResource Margin4}" | ||||
|                     VerticalAlignment="Center" | ||||
|                     Style="{StaticResource ToolbarTextBlock}" | ||||
|                     Text="{x:Static resx:ResUI.TbRemarks}" /> | ||||
|                 <TextBox | ||||
|                     x:Name="txtRemarks" | ||||
|                     Grid.Row="1" | ||||
|                     Grid.Column="1" | ||||
|                     Width="400" | ||||
|                     Margin="{StaticResource Margin4}" | ||||
|                     Style="{StaticResource DefTextBox}" /> | ||||
| 
 | ||||
|                 <TextBlock | ||||
|                     Grid.Row="2" | ||||
|                     Grid.Column="0" | ||||
|                     Margin="{StaticResource Margin4}" | ||||
|                     VerticalAlignment="Center" | ||||
|                     Style="{StaticResource ToolbarTextBlock}" | ||||
|                     Text="{x:Static resx:ResUI.TbCoreType}" /> | ||||
|                 <ComboBox | ||||
|                     x:Name="cmbCoreType" | ||||
|                     Grid.Row="2" | ||||
|                     Grid.Column="1" | ||||
|                     Width="200" | ||||
|                     Margin="{StaticResource Margin4}" | ||||
|                     materialDesign:HintAssist.Hint="{x:Static resx:ResUI.TbCoreType}" | ||||
|                     Style="{StaticResource DefComboBox}" /> | ||||
| 
 | ||||
|                 <Grid | ||||
|                     x:Name="gridPolicyGroup" | ||||
|                     Grid.Row="3" | ||||
|                     Grid.Column="0" | ||||
|                     Grid.ColumnSpan="3"> | ||||
|                     <Grid.ColumnDefinitions> | ||||
|                         <ColumnDefinition Width="180" /> | ||||
|                         <ColumnDefinition Width="Auto" /> | ||||
|                         <ColumnDefinition Width="Auto" /> | ||||
|                     </Grid.ColumnDefinitions> | ||||
|                     <TextBlock | ||||
|                         Grid.Row="3" | ||||
|                         Grid.Column="0" | ||||
|                         Margin="{StaticResource Margin4}" | ||||
|                         VerticalAlignment="Center" | ||||
|                         Style="{StaticResource ToolbarTextBlock}" | ||||
|                         Text="{x:Static resx:ResUI.TbPolicyGroupType}" /> | ||||
|                     <ComboBox | ||||
|                         x:Name="cmbPolicyGroupType" | ||||
|                         Grid.Row="3" | ||||
|                         Grid.Column="1" | ||||
|                         Width="200" | ||||
|                         Margin="{StaticResource Margin4}" | ||||
|                         materialDesign:HintAssist.Hint="{x:Static resx:ResUI.TbPolicyGroupType}" | ||||
|                         Style="{StaticResource DefComboBox}" /> | ||||
|                 </Grid> | ||||
|             </Grid> | ||||
|         </Grid> | ||||
|         <TabControl> | ||||
|             <TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList}"> | ||||
|                 <DataGrid | ||||
|                     x:Name="lstChild" | ||||
|                     AutoGenerateColumns="False" | ||||
|                     BorderThickness="1" | ||||
|                     CanUserAddRows="False" | ||||
|                     CanUserResizeRows="False" | ||||
|                     CanUserSortColumns="False" | ||||
|                     EnableRowVirtualization="True" | ||||
|                     GridLinesVisibility="All" | ||||
|                     HeadersVisibility="Column" | ||||
|                     IsReadOnly="True" | ||||
|                     Style="{StaticResource DefDataGrid}"> | ||||
|                     <DataGrid.ContextMenu> | ||||
|                         <ContextMenu Style="{StaticResource DefContextMenu}"> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuAddChildServer" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Click="MenuAddChild_Click" | ||||
|                                 Header="{x:Static resx:ResUI.menuAddChildServer}" /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuRemoveChildServer" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuRemoveChildServer}" /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuSelectAllChild" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuSelectAll}" /> | ||||
|                             <Separator /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuMoveTop" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuMoveTop}" /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuMoveUp" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuMoveUp}" /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuMoveDown" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuMoveDown}" /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuMoveBottom" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 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> | ||||
|             </TabItem> | ||||
|         </TabControl> | ||||
|     </DockPanel> | ||||
| </base:WindowBase> | ||||
							
								
								
									
										148
									
								
								v2rayN/v2rayN/Views/AddGroupServerWindow.xaml.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								v2rayN/v2rayN/Views/AddGroupServerWindow.xaml.cs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,148 @@ | |||
| using System.Reactive.Disposables; | ||||
| using System.Windows; | ||||
| using System.Windows.Input; | ||||
| using DynamicData; | ||||
| using ReactiveUI; | ||||
| 
 | ||||
| namespace v2rayN.Views; | ||||
| 
 | ||||
| public partial class AddGroupServerWindow | ||||
| { | ||||
|     public AddGroupServerWindow(ProfileItem profileItem) | ||||
|     { | ||||
|         InitializeComponent(); | ||||
| 
 | ||||
|         this.Owner = Application.Current.MainWindow; | ||||
|         this.Loaded += Window_Loaded; | ||||
|         this.PreviewKeyDown += AddGroupServerWindow_PreviewKeyDown; | ||||
|         lstChild.SelectionChanged += LstChild_SelectionChanged; | ||||
|         menuSelectAllChild.Click += MenuSelectAllChild_Click; | ||||
| 
 | ||||
|         ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler); | ||||
| 
 | ||||
|         cmbCoreType.ItemsSource = Global.CoreTypes; | ||||
|         cmbPolicyGroupType.ItemsSource = new List<string> | ||||
|         { | ||||
|             ResUI.TbLeastPing, | ||||
|             ResUI.TbFallback, | ||||
|             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.Visibility = Visibility.Collapsed; | ||||
|                 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.Text).DisposeWith(disposables); | ||||
|             this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.Text).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); | ||||
|         }); | ||||
|         WindowsUtils.SetDarkBorder(this, AppManager.Instance.Config.UiItem.CurrentTheme); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) | ||||
|     { | ||||
|         switch (action) | ||||
|         { | ||||
|             case EViewAction.CloseWindow: | ||||
|                 this.DialogResult = true; | ||||
|                 break; | ||||
|         } | ||||
|         return await Task.FromResult(true); | ||||
|     } | ||||
| 
 | ||||
|     private void Window_Loaded(object sender, RoutedEventArgs e) | ||||
|     { | ||||
|         txtRemarks.Focus(); | ||||
|     } | ||||
| 
 | ||||
|     private void AddGroupServerWindow_PreviewKeyDown(object sender, KeyEventArgs e) | ||||
|     { | ||||
|         if (!lstChild.IsKeyboardFocusWithin) | ||||
|             return; | ||||
|         if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) | ||||
|         { | ||||
|             if (e.Key == Key.A) | ||||
|             { | ||||
|                 lstChild.SelectAll(); | ||||
|             } | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             if (e.Key == Key.T) | ||||
|             { | ||||
|                 ViewModel?.MoveServer(EMove.Top); | ||||
|             } | ||||
|             else if (e.Key == Key.U) | ||||
|             { | ||||
|                 ViewModel?.MoveServer(EMove.Up); | ||||
|             } | ||||
|             else if (e.Key == Key.D) | ||||
|             { | ||||
|                 ViewModel?.MoveServer(EMove.Down); | ||||
|             } | ||||
|             else if (e.Key == Key.B) | ||||
|             { | ||||
|                 ViewModel?.MoveServer(EMove.Bottom); | ||||
|             } | ||||
|             else if (e.Key == Key.Delete) | ||||
|             { | ||||
|                 ViewModel?.ChildRemoveAsync(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async void MenuAddChild_Click(object sender, RoutedEventArgs e) | ||||
|     { | ||||
|         var selectWindow = new ProfilesSelectWindow(); | ||||
|         if (ViewModel?.SelectedSource?.ConfigType == EConfigType.PolicyGroup) | ||||
|         { | ||||
|             selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true); | ||||
|         } | ||||
|         selectWindow.AllowMultiSelect(true); | ||||
|         if (selectWindow.ShowDialog() == true) | ||||
|         { | ||||
|             var profiles = await selectWindow.ProfileItems; | ||||
|             ViewModel?.ChildItemsObs.AddRange(profiles); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void LstChild_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) | ||||
|     { | ||||
|         if (ViewModel != null) | ||||
|         { | ||||
|             ViewModel.SelectedChildren = lstChild.SelectedItems.Cast<ProfileItem>().ToList(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void MenuSelectAllChild_Click(object sender, RoutedEventArgs e) | ||||
|     { | ||||
|         lstChild.SelectAll(); | ||||
|     } | ||||
| } | ||||
|  | @ -71,6 +71,14 @@ | |||
|                                     x:Name="menuAddCustomServer" | ||||
|                                     Height="{StaticResource MenuItemHeight}" | ||||
|                                     Header="{x:Static resx:ResUI.menuAddCustomServer}" /> | ||||
|                                 <MenuItem | ||||
|                                     x:Name="menuAddPolicyGroupServer" | ||||
|                                     Height="{StaticResource MenuItemHeight}" | ||||
|                                     Header="{x:Static resx:ResUI.menuAddPolicyGroupServer}" /> | ||||
|                                 <MenuItem | ||||
|                                     x:Name="menuAddProxyChainServer" | ||||
|                                     Height="{StaticResource MenuItemHeight}" | ||||
|                                     Header="{x:Static resx:ResUI.menuAddProxyChainServer}" /> | ||||
|                                 <Separator Margin="-40,5" /> | ||||
|                                 <MenuItem | ||||
|                                     x:Name="menuAddVmessServer" | ||||
|  |  | |||
|  | @ -79,6 +79,8 @@ public partial class MainWindow | |||
|             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); | ||||
|  | @ -195,6 +197,11 @@ public partial class MainWindow | |||
|                     return false; | ||||
|                 return (new AddServer2Window((ProfileItem)obj)).ShowDialog() ?? false; | ||||
| 
 | ||||
|             case EViewAction.AddGroupServerWindow: | ||||
|                 if (obj is null) | ||||
|                     return false; | ||||
|                 return (new AddGroupServerWindow((ProfileItem)obj)).ShowDialog() ?? false; | ||||
| 
 | ||||
|             case EViewAction.DNSSettingWindow: | ||||
|                 return (new DNSSettingWindow().ShowDialog() ?? false); | ||||
| 
 | ||||
|  |  | |||
|  | @ -125,28 +125,32 @@ | |||
|                             Height="{StaticResource MenuItemHeight}" | ||||
|                             Header="{x:Static resx:ResUI.menuShareServer}" /> | ||||
|                         <Separator /> | ||||
|                         <MenuItem Header="{x:Static resx:ResUI.menuSetDefaultMultipleServer}"> | ||||
|                         <MenuItem Header="{x:Static resx:ResUI.menuGenGroupMultipleServer}"> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuSetDefaultMultipleServerXrayRandom" | ||||
|                                 x:Name="menuGenGroupMultipleServerXrayRandom" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayRandom}" /> | ||||
|                                 Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRandom}" /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuSetDefaultMultipleServerXrayRoundRobin" | ||||
|                                 x:Name="menuGenGroupMultipleServerXrayRoundRobin" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayRoundRobin}" /> | ||||
|                                 Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRoundRobin}" /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuSetDefaultMultipleServerXrayLeastPing" | ||||
|                                 x:Name="menuGenGroupMultipleServerXrayLeastPing" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayLeastPing}" /> | ||||
|                                 Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastPing}" /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuSetDefaultMultipleServerXrayLeastLoad" | ||||
|                                 x:Name="menuGenGroupMultipleServerXrayLeastLoad" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayLeastLoad}" /> | ||||
|                                 Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastLoad}" /> | ||||
|                             <Separator /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuSetDefaultMultipleServerSingBoxLeastPing" | ||||
|                                 x:Name="menuGenGroupMultipleServerSingBoxLeastPing" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerSingBoxLeastPing}" /> | ||||
|                                 Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxLeastPing}" /> | ||||
|                             <MenuItem | ||||
|                                 x:Name="menuGenGroupMultipleServerSingBoxFallback" | ||||
|                                 Height="{StaticResource MenuItemHeight}" | ||||
|                                 Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxFallback}" /> | ||||
|                         </MenuItem> | ||||
|                         <Separator /> | ||||
|                         <MenuItem | ||||
|  |  | |||
|  | @ -60,11 +60,12 @@ public partial class ProfilesView | |||
|             this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRandomCmd, v => v.menuSetDefaultMultipleServerXrayRandom).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRoundRobinCmd, v => v.menuSetDefaultMultipleServerXrayRoundRobin).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastPingCmd, v => v.menuSetDefaultMultipleServerXrayLeastPing).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastLoadCmd, v => v.menuSetDefaultMultipleServerXrayLeastLoad).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerSingBoxLeastPingCmd, v => v.menuSetDefaultMultipleServerSingBoxLeastPing).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables); | ||||
|             this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables); | ||||
| 
 | ||||
|             //servers move | ||||
|             this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); | ||||
|  | @ -148,6 +149,11 @@ public partial class ProfilesView | |||
|                     return false; | ||||
|                 return (new AddServer2Window((ProfileItem)obj)).ShowDialog() ?? false; | ||||
| 
 | ||||
|             case EViewAction.AddGroupServerWindow: | ||||
|                 if (obj is null) | ||||
|                     return false; | ||||
|                 return (new AddGroupServerWindow((ProfileItem)obj)).ShowDialog() ?? false; | ||||
| 
 | ||||
|             case EViewAction.ShareServer: | ||||
|                 if (obj is null) | ||||
|                     return false; | ||||
|  |  | |||
|  | @ -57,7 +57,7 @@ public partial class SubEditWindow | |||
|     private async void BtnSelectPrevProfile_Click(object sender, RoutedEventArgs e) | ||||
|     { | ||||
|         var selectWindow = new ProfilesSelectWindow(); | ||||
|         selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true); | ||||
|         selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true); | ||||
|         if (selectWindow.ShowDialog() == true) | ||||
|         { | ||||
|             var profile = await selectWindow.ProfileItem; | ||||
|  | @ -71,7 +71,7 @@ public partial class SubEditWindow | |||
|     private async void BtnSelectNextProfile_Click(object sender, RoutedEventArgs e) | ||||
|     { | ||||
|         var selectWindow = new ProfilesSelectWindow(); | ||||
|         selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true); | ||||
|         selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true); | ||||
|         if (selectWindow.ShowDialog() == true) | ||||
|         { | ||||
|             var profile = await selectWindow.ProfileItem; | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue