From c20d5b32081844124c871c58ee65c5b03b0a06fe Mon Sep 17 00:00:00 2001 From: DHR60 Date: Wed, 10 Sep 2025 12:38:29 +0800 Subject: [PATCH 01/17] Add global fakeip and fakeip filter --- v2rayN/ServiceLib/Global.cs | 1 + v2rayN/ServiceLib/Handler/ConfigHandler.cs | 5 + v2rayN/ServiceLib/Models/ConfigItems.cs | 1 + v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 18 ++-- v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 6 +- v2rayN/ServiceLib/Resx/ResUI.hu.resx | 6 +- v2rayN/ServiceLib/Resx/ResUI.resx | 6 +- v2rayN/ServiceLib/Resx/ResUI.ru.resx | 6 +- v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 6 +- v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 6 +- .../ServiceLib/Sample/singbox_fakeip_filter | 92 +++++++++++++++++++ v2rayN/ServiceLib/ServiceLib.csproj | 1 + .../CoreConfig/Singbox/SingboxDnsService.cs | 35 ++++++- .../Views/DNSSettingWindow.axaml | 2 +- v2rayN/v2rayN/Views/DNSSettingWindow.xaml | 2 +- 15 files changed, 163 insertions(+), 30 deletions(-) create mode 100644 v2rayN/ServiceLib/Sample/singbox_fakeip_filter diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index a7c74c08..eb24d71e 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -40,6 +40,7 @@ public class Global public const string ProxySetLinuxShellFileName = NamespaceSample + "proxy_set_linux_sh"; public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh"; public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh"; + public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter"; public const string DefaultSecurity = "auto"; public const string DefaultNetwork = "tcp"; diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index e488720f..786f3aff 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -113,6 +113,10 @@ public static class ConfigHandler config.ConstItem ??= new ConstItem(); config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); + if (config.SimpleDNSItem.GlobalFakeIp is null) + { + config.SimpleDNSItem.GlobalFakeIp = true; + } config.SpeedTestItem ??= new(); if (config.SpeedTestItem.SpeedTestTimeout < 10) @@ -2221,6 +2225,7 @@ public static class ConfigHandler UseSystemHosts = false, AddCommonHosts = true, FakeIP = false, + GlobalFakeIp = true, BlockBindingQuery = true, DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(), RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(), diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index 148f2bd7..b9b305bb 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -260,6 +260,7 @@ public class SimpleDNSItem public bool? UseSystemHosts { get; set; } public bool? AddCommonHosts { get; set; } public bool? FakeIP { get; set; } + public bool? GlobalFakeIp { get; set; } public bool? BlockBindingQuery { get; set; } public string? DirectDNS { get; set; } public string? RemoteDNS { get; set; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 6503b41c..7e848cf0 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -2301,15 +2301,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Apply to Proxy Domains Only 的本地化字符串。 - /// - public static string TbApplyProxyDomainsOnly { - get { - return ResourceManager.GetString("TbApplyProxyDomainsOnly", resourceCulture); - } - } - /// /// 查找类似 Auto refresh 的本地化字符串。 /// @@ -2526,6 +2517,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Applies globally by default, with built-in FakeIP filtering (sing-box only). 的本地化字符串。 + /// + public static string TbFakeIPTips { + get { + return ResourceManager.GetString("TbFakeIPTips", resourceCulture); + } + } + /// /// 查找类似 Fingerprint 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 0c8ca393..f79901eb 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1455,9 +1455,6 @@ DNS Hosts: ("domain1 ip1 ip2" per line) - - Apply to Proxy Domains Only - Basic DNS Settings @@ -1515,4 +1512,7 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 800fa00f..e801c796 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1455,9 +1455,6 @@ DNS Hosts: ("domain1 ip1 ip2" per line) - - Apply to Proxy Domains Only - Basic DNS Settings @@ -1515,4 +1512,7 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 614c8092..fc4b218f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1455,9 +1455,6 @@ DNS Hosts: ("domain1 ip1 ip2" per line) - - Apply to Proxy Domains Only - Basic DNS Settings @@ -1515,4 +1512,7 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 294d9f34..04e16eb8 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1455,9 +1455,6 @@ DNS hosts: (каждая строка в формате "domain1 ip1 ip2") - - Применять только к доменам через прокси - Базовые настройки DNS @@ -1515,4 +1512,7 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 194a59e2..ced2d74b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1452,9 +1452,6 @@ DNS Hosts:(“域名1 ip1 ip2” 一行一个) - - 仅对代理域名生效 - DNS 基础设置 @@ -1512,4 +1509,7 @@ 选择配置文件 + + 默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index fa84c789..720b7269 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1452,9 +1452,6 @@ DNS Hosts: ("domain1 ip1 ip2" per line) - - Apply to Proxy Domains Only - Basic DNS Settings @@ -1512,4 +1509,7 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/singbox_fakeip_filter b/v2rayN/ServiceLib/Sample/singbox_fakeip_filter new file mode 100644 index 00000000..860ab0eb --- /dev/null +++ b/v2rayN/ServiceLib/Sample/singbox_fakeip_filter @@ -0,0 +1,92 @@ +{ + "domain": [ + "amobile.music.tc.qq.com", + "api-jooxtt.sanook.com", + "api.joox.com", + "aqqmusic.tc.qq.com", + "dl.stream.qqmusic.qq.com", + "ff.dorado.sdo.com", + "heartbeat.belkin.com", + "isure.stream.qqmusic.qq.com", + "joox.com", + "lens.l.google.com", + "localhost.ptlogin2.qq.com", + "localhost.sec.qq.com", + "mesu.apple.com", + "mobileoc.music.tc.qq.com", + "music.taihe.com", + "musicapi.taihe.com", + "na.b.g-tun.com", + "proxy.golang.org", + "ps.res.netease.com", + "shark007.net", + "songsearch.kugou.com", + "static.adtidy.org", + "streamoc.music.tc.qq.com", + "swcdn.apple.com", + "swdist.apple.com", + "swdownload.apple.com", + "swquery.apple.com", + "swscan.apple.com", + "trackercdn.kugou.com", + "xnotify.xboxlive.com" + ], + "domain_keyword": [ + "ntp", + "stun", + "time" + ], + "domain_regex": [ + "^[^.]+$", + "^[^.]+\\.[^.]+\\.xboxlive\\.com$", + "^localhost\\.[^.]+\\.weixin\\.qq\\.com$", + "^mijia\\scloud$", + "^xbox\\.[^.]+\\.microsoft\\.com$", + "^xbox\\.[^.]+\\.[^.]+\\.microsoft\\.com$" + ], + "domain_suffix": [ + "126.net", + "3gppnetwork.org", + "battle.net", + "battlenet.com.cn", + "cdn.nintendo.net", + "cmbchina.com", + "cmbimg.com", + "ff14.sdo.com", + "ffxiv.com", + "finalfantasyxiv.com", + "gcloudcs.com", + "home.arpa", + "invalid", + "kuwo.cn", + "lan", + "linksys.com", + "linksyssmartwifi.com", + "local", + "localdomain", + "localhost", + "market.xiaomi.com", + "mcdn.bilivideo.cn", + "media.dssott.com", + "msftconnecttest.com", + "msftncsi.com", + "music.163.com", + "music.migu.cn", + "n0808.com", + "nflxvideo.net", + "oray.com", + "orayimg.com", + "router.asus.com", + "sandai.net", + "square-enix.com", + "srv.nintendo.net", + "steamcontent.com", + "uu.163.com", + "wargaming.net", + "wggames.cn", + "wotgame.cn", + "wowsgame.cn", + "xiami.com", + "y.qq.com" + ] +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj index ecbab780..7ee72196 100644 --- a/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -44,6 +44,7 @@ + diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index 306572a1..00bb14d9 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -33,6 +33,15 @@ public partial class CoreConfigSingboxService lastRule.Ip?.Contains("0.0.0.0/0") == true); } singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag; + if ((!useDirectDns) && simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false) + { + singboxConfig.dns.rules.Add(new() + { + server = Global.SingboxFakeDNSTag, + query_type = new List { 1, 28 }, // A and AAAA + rewrite_ttl = 1, + }); + } // Tun2SocksAddress if (node != null && Utils.IsDomain(node.Address)) @@ -187,6 +196,28 @@ public partial class CoreConfigSingboxService }); } + if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == true) + { + var fakeipFilterRule = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName)); + fakeipFilterRule.invert = true; + var rule4Fake = new Rule4Sbox + { + server = Global.SingboxFakeDNSTag, + type = "logical", + mode = "and", + rewrite_ttl = 1, + rules = new List + { + new() { + query_type = new List { 1, 28 }, // A and AAAA + }, + fakeipFilterRule, + } + }; + + singboxConfig.dns.rules.Add(rule4Fake); + } + var routing = await ConfigHandler.GetDefaultRouting(_config); if (routing == null) return 0; @@ -266,10 +297,12 @@ public partial class CoreConfigSingboxService } else { - if (simpleDNSItem.FakeIP == true) + if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false) { var rule4Fake = JsonUtils.DeepCopy(rule); rule4Fake.server = Global.SingboxFakeDNSTag; + rule4Fake.query_type = new List { 1, 28 }; // A and AAAA + rule4Fake.rewrite_ttl = 1; singboxConfig.dns.rules.Add(rule4Fake); } rule.server = Global.SingboxRemoteDNSTag; diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml index 18f294d3..d8eed12e 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml @@ -229,7 +229,7 @@ Grid.Column="2" Margin="{StaticResource Margin4}" VerticalAlignment="Center" - Text="{x:Static resx:ResUI.TbApplyProxyDomainsOnly}" + Text="{x:Static resx:ResUI.TbFakeIPTips}" TextWrapping="Wrap" /> Date: Sat, 6 Sep 2025 20:12:04 +0800 Subject: [PATCH 02/17] Multi Profile --- v2rayN/ServiceLib/Enums/EConfigType.cs | 6 +- v2rayN/ServiceLib/Enums/EMultipleLoad.cs | 2 +- v2rayN/ServiceLib/Handler/ConfigHandler.cs | 38 ++++- v2rayN/ServiceLib/Manager/AppManager.cs | 11 ++ .../Manager/ProfileGroupItemManager.cs | 156 ++++++++++++++++++ v2rayN/ServiceLib/Manager/TaskManager.cs | 1 + v2rayN/ServiceLib/Models/ProfileGroupItem.cs | 13 ++ v2rayN/ServiceLib/Models/ProfileItem.cs | 11 +- .../ViewModels/AddGroupServerViewModel.cs | 17 ++ .../ViewModels/MainWindowViewModel.cs | 1 + 10 files changed, 248 insertions(+), 8 deletions(-) create mode 100644 v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs create mode 100644 v2rayN/ServiceLib/Models/ProfileGroupItem.cs create mode 100644 v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs diff --git a/v2rayN/ServiceLib/Enums/EConfigType.cs b/v2rayN/ServiceLib/Enums/EConfigType.cs index 6698f962..5ddb8451 100644 --- a/v2rayN/ServiceLib/Enums/EConfigType.cs +++ b/v2rayN/ServiceLib/Enums/EConfigType.cs @@ -12,5 +12,9 @@ public enum EConfigType TUIC = 8, WireGuard = 9, HTTP = 10, - Anytls = 11 + Anytls = 11, + + Group = 1000, + PolicyGroup = 1001, + ProxyChain = 1002, } diff --git a/v2rayN/ServiceLib/Enums/EMultipleLoad.cs b/v2rayN/ServiceLib/Enums/EMultipleLoad.cs index 42be7a5b..af42fb60 100644 --- a/v2rayN/ServiceLib/Enums/EMultipleLoad.cs +++ b/v2rayN/ServiceLib/Enums/EMultipleLoad.cs @@ -2,8 +2,8 @@ namespace ServiceLib.Enums; public enum EMultipleLoad { + LeastPing, Random, RoundRobin, - LeastPing, LeastLoad } diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 786f3aff..e8ab587a 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -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 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; + } + /// /// Compare two profile items to determine if they represent the same server /// Used for deduplication and server matching @@ -1207,7 +1241,7 @@ public static class ConfigHandler public static async Task GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType) { ProfileItem? itemSocks = null; - if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun) + if (node.ConfigType != EConfigType.Custom && node.ConfigType < EConfigType.Group && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun) { itemSocks = new ProfileItem() { @@ -1218,7 +1252,7 @@ public static class ConfigHandler Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks) }; } - else if ((node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)) + else if ((node.ConfigType == EConfigType.Custom && node.ConfigType < EConfigType.Group && node.PreSocksPort > 0)) { var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; itemSocks = new ProfileItem() diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index 33125289..8a86133f 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -67,6 +67,7 @@ public sealed class AppManager SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); + SQLiteHelper.Instance.CreateTable(); 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(); @@ -219,6 +221,15 @@ public sealed class AppManager return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.Remarks == remarks); } + public async Task GetProfileGroupItem(string parentIndexId) + { + if (parentIndexId.IsNullOrEmpty()) + { + return null; + } + return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.ParentIndexId == parentIndexId); + } + public async Task?> RoutingItems() { return await SQLiteHelper.Instance.TableAsync().OrderBy(t => t.Sort).ToListAsync(); diff --git a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs new file mode 100644 index 00000000..7a35b671 --- /dev/null +++ b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs @@ -0,0 +1,156 @@ +using System.Collections.Concurrent; + +namespace ServiceLib.Manager; + +public class ProfileGroupItemManager +{ + private static readonly Lazy _instance = new(() => new()); + private ConcurrentDictionary _items = new(); + + public static ProfileGroupItemManager Instance => _instance.Value; + private static readonly string _tag = "ProfileGroupItemManager"; + + private ProfileGroupItemManager() + { + } + + public async Task Init() + { + await InitData(); + } + + public Task> GetProfileGroupItemList() + { + var bag = new ConcurrentBag(_items.Values); + return Task.FromResult(bag); + } + + 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().ToListAsync(); + _items = new ConcurrentDictionary(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)) + { + throw new ArgumentNullException(nameof(indexId)); + } + + 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().ToListAsync(); + var existsMap = lstExists.Where(t => !string.IsNullOrEmpty(t.ParentIndexId)).ToDictionary(t => t.ParentIndexId!); + + var lstInserts = new List(); + var lstUpdates = new List(); + + 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().Where(t => t.ParentIndexId == item.ParentIndexId).ToListAsync(); + if (lst != null && lst.Count > 0) + { + await SQLiteHelper.Instance.UpdateAllAsync(new List { item }); + } + else + { + await SQLiteHelper.Instance.InsertAllAsync(new List { item }); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } +} diff --git a/v2rayN/ServiceLib/Manager/TaskManager.cs b/v2rayN/ServiceLib/Manager/TaskManager.cs index dfbd242d..c626e1b7 100644 --- a/v2rayN/ServiceLib/Manager/TaskManager.cs +++ b/v2rayN/ServiceLib/Manager/TaskManager.cs @@ -35,6 +35,7 @@ public class TaskManager await ConfigHandler.SaveConfig(_config); await ProfileExManager.Instance.SaveTo(); + await ProfileGroupItemManager.Instance.SaveTo(); } //Execute once 1 hour diff --git a/v2rayN/ServiceLib/Models/ProfileGroupItem.cs b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs new file mode 100644 index 00000000..eb6ac49f --- /dev/null +++ b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs @@ -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; +} diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs index 998aa120..45c8df8d 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileItem.cs @@ -39,11 +39,14 @@ public class ProfileItem : ReactiveObject > 1 => $"***{arrAddr.Last()}", _ => Address }; - summary += ConfigType switch + if (ConfigType is EConfigType.Custom or > EConfigType.Group) { - EConfigType.Custom => $"[{CoreType.ToString()}]{Remarks}", - _ => $"{Remarks}({addr}:{Port})" - }; + summary += $"[{CoreType.ToString()}]{Remarks}"; + } + else + { + summary += $"{Remarks}({addr}:{Port})"; + } return summary; } diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs new file mode 100644 index 00000000..5d10160c --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -0,0 +1,17 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class AddGroupServerViewModel : MyReactiveObject +{ + [Reactive] + public ProfileItem SelectedSource { get; set; } + + [Reactive] + public IList SelectedChildren { get; set; } + + //[Reactive] + //public +} diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 4e9fc9fc..83a91ce5 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -228,6 +228,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); From 68027332fe0da567ecd7d91f46695e7f327094c1 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Wed, 10 Sep 2025 23:44:42 +0800 Subject: [PATCH 03/17] VM and wpf --- v2rayN/ServiceLib/Enums/EViewAction.cs | 1 + .../Manager/ProfileGroupItemManager.cs | 19 +- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 108 +++++++++ v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 36 +++ v2rayN/ServiceLib/Resx/ResUI.hu.resx | 36 +++ v2rayN/ServiceLib/Resx/ResUI.resx | 36 +++ v2rayN/ServiceLib/Resx/ResUI.ru.resx | 36 +++ v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 36 +++ v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 36 +++ .../ViewModels/AddGroupServerViewModel.cs | 199 +++++++++++++++- .../ViewModels/MainWindowViewModel.cs | 14 ++ .../ViewModels/ProfilesViewModel.cs | 4 + v2rayN/v2rayN/Views/AddGroupServerWindow.xaml | 216 ++++++++++++++++++ .../v2rayN/Views/AddGroupServerWindow.xaml.cs | 126 ++++++++++ v2rayN/v2rayN/Views/MainWindow.xaml | 8 + v2rayN/v2rayN/Views/MainWindow.xaml.cs | 7 + v2rayN/v2rayN/Views/ProfilesView.xaml.cs | 5 + 17 files changed, 917 insertions(+), 6 deletions(-) create mode 100644 v2rayN/v2rayN/Views/AddGroupServerWindow.xaml create mode 100644 v2rayN/v2rayN/Views/AddGroupServerWindow.xaml.cs diff --git a/v2rayN/ServiceLib/Enums/EViewAction.cs b/v2rayN/ServiceLib/Enums/EViewAction.cs index 2c47d31d..e404d071 100644 --- a/v2rayN/ServiceLib/Enums/EViewAction.cs +++ b/v2rayN/ServiceLib/Enums/EViewAction.cs @@ -24,6 +24,7 @@ public enum EViewAction RoutingRuleDetailsWindow, AddServerWindow, AddServer2Window, + AddGroupServerWindow, DNSSettingWindow, RoutingSettingWindow, OptionSettingWindow, diff --git a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs index 7a35b671..4812a0d2 100644 --- a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs +++ b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs @@ -19,10 +19,21 @@ public class ProfileGroupItemManager await InitData(); } - public Task> GetProfileGroupItemList() + // Read-only getters: do not create or mark dirty + public bool TryGet(string indexId, out ProfileGroupItem? item) { - var bag = new ConcurrentBag(_items.Values); - return Task.FromResult(bag); + item = null; + if (string.IsNullOrWhiteSpace(indexId)) + { + return false; + } + + return _items.TryGetValue(indexId, out item); + } + + public ProfileGroupItem? GetOrDefault(string indexId) + { + return string.IsNullOrWhiteSpace(indexId) ? null : (_items.TryGetValue(indexId, out var v) ? v : null); } private async Task InitData() @@ -50,7 +61,7 @@ public class ProfileGroupItemManager { if (string.IsNullOrEmpty(indexId)) { - throw new ArgumentNullException(nameof(indexId)); + indexId = Utils.GetGuid(false); } return _items.GetOrAdd(indexId, AddProfileGroupItem); diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 7e848cf0..3cfddc1c 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -672,6 +672,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Child Configuration 的本地化字符串。 + /// + public static string menuAddChildServer { + get { + return ResourceManager.GetString("menuAddChildServer", resourceCulture); + } + } + /// /// 查找类似 Add a custom configuration Configuration 的本地化字符串。 /// @@ -699,6 +708,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Policy Group Configuration 的本地化字符串。 + /// + public static string menuAddPolicyGroupServer { + get { + return ResourceManager.GetString("menuAddPolicyGroupServer", resourceCulture); + } + } + + /// + /// 查找类似 Add Proxy Chain Configuration 的本地化字符串。 + /// + public static string menuAddProxyChainServer { + get { + return ResourceManager.GetString("menuAddProxyChainServer", resourceCulture); + } + } + /// /// 查找类似 Import Share Links from clipboard (Ctrl+V) 的本地化字符串。 /// @@ -1320,6 +1347,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Remove Child Configuration 的本地化字符串。 + /// + public static string menuRemoveChildServer { + get { + return ResourceManager.GetString("menuRemoveChildServer", resourceCulture); + } + } + /// /// 查找类似 Remove duplicate Configurations 的本地化字符串。 /// @@ -1995,6 +2031,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please Add At Least One Configuration 的本地化字符串。 + /// + public static string PleaseAddAtLeastOneServer { + get { + return ResourceManager.GetString("PleaseAddAtLeastOneServer", resourceCulture); + } + } + /// /// 查找类似 Please fill Remarks 的本地化字符串。 /// @@ -2373,6 +2418,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group 的本地化字符串。 + /// + public static string TbConfigTypePolicyGroup { + get { + return ResourceManager.GetString("TbConfigTypePolicyGroup", resourceCulture); + } + } + + /// + /// 查找类似 Proxy Chain 的本地化字符串。 + /// + public static string TbConfigTypeProxyChain { + get { + return ResourceManager.GetString("TbConfigTypeProxyChain", resourceCulture); + } + } + /// /// 查找类似 Confirm 的本地化字符串。 /// @@ -2643,6 +2706,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Most Stable 的本地化字符串。 + /// + public static string TbLeastLoad { + get { + return ResourceManager.GetString("TbLeastLoad", resourceCulture); + } + } + + /// + /// 查找类似 Lowest Latency 的本地化字符串。 + /// + public static string TbLeastPing { + get { + return ResourceManager.GetString("TbLeastPing", resourceCulture); + } + } + /// /// 查找类似 Address (IPv4, IPv6) 的本地化字符串。 /// @@ -2697,6 +2778,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group Type 的本地化字符串。 + /// + public static string TbPolicyGroupType { + get { + return ResourceManager.GetString("TbPolicyGroupType", resourceCulture); + } + } + /// /// 查找类似 Port 的本地化字符串。 /// @@ -2769,6 +2859,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Random 的本地化字符串。 + /// + public static string TbRandom { + get { + return ResourceManager.GetString("TbRandom", resourceCulture); + } + } + /// /// 查找类似 v2ray Full Config Template 的本地化字符串。 /// @@ -2832,6 +2931,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Round Robin 的本地化字符串。 + /// + public static string TbRoundRobin { + get { + return ResourceManager.GetString("TbRoundRobin", resourceCulture); + } + } + /// /// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index f79901eb..4f4d375e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1515,4 +1515,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index e801c796..46b5b781 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1515,4 +1515,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index fc4b218f..4bf11dcd 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1515,4 +1515,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 04e16eb8..c55c3826 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1515,4 +1515,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index ced2d74b..3058f419 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1512,4 +1512,40 @@ 默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效 + + 请至少添加一个配置文件 + + + 策略组 + + + 链式代理 + + + 最低延迟 + + + 随机 + + + 负载均衡 + + + 最稳定 + + + 策略组类型 + + + 添加策略组配置文件 + + + 添加链式代理配置文件 + + + 添加子配置文件 + + + 删除子配置文件 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 720b7269..eec7fb77 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1512,4 +1512,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs index 5d10160c..2a7f13f3 100644 --- a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -1,4 +1,5 @@ using System.Reactive; +using DynamicData.Binding; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -9,9 +10,203 @@ public class AddGroupServerViewModel : MyReactiveObject [Reactive] public ProfileItem SelectedSource { get; set; } + [Reactive] + public ProfileItem SelectedChild { get; set; } + [Reactive] public IList SelectedChildren { get; set; } - //[Reactive] - //public + [Reactive] + public string? CoreType { get; set; } + + [Reactive] + public string? PolicyGroupType { get; set; } + + public IObservableCollection ChildItemsObs { get; } = new ObservableCollectionExtended(); + + //public ReactiveCommand AddCmd { get; } + public ReactiveCommand RemoveCmd { get; } + public ReactiveCommand MoveTopCmd { get; } + public ReactiveCommand MoveUpCmd { get; } + public ReactiveCommand MoveDownCmd { get; } + public ReactiveCommand MoveBottomCmd { get; } + + public ReactiveCommand SaveCmd { get; } + + public AddGroupServerViewModel(ProfileItem profileItem, Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + RemoveCmd = ReactiveCommand.CreateFromTask(async () => + { + await ChildRemoveAsync(); + }); + MoveTopCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Top); + }); + MoveUpCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Up); + }); + MoveDownCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Down); + }); + MoveBottomCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Bottom); + }); + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveServerAsync(); + }); + + SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem); + + CoreType = (SelectedSource?.CoreType ?? ECoreType.Xray).ToString(); + + ProfileGroupItemManager.Instance.TryGet(profileItem.IndexId, out var profileGroup); + PolicyGroupType = (profileGroup?.MultipleLoad ?? EMultipleLoad.LeastPing) switch + { + EMultipleLoad.LeastPing => ResUI.TbLeastPing, + EMultipleLoad.Random => ResUI.TbRandom, + EMultipleLoad.RoundRobin => ResUI.TbRoundRobin, + EMultipleLoad.LeastLoad => ResUI.TbLeastLoad, + _ => ResUI.TbLeastPing, + }; + + _ = Init(); + } + + public async Task Init() + { + var childItemMulti = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource?.IndexId); + if (childItemMulti != null) + { + var childIndexIds = childItemMulti.ChildItems.IsNullOrEmpty() ? new List() : Utils.String2List(childItemMulti.ChildItems); + foreach (var item in childIndexIds) + { + var child = await AppManager.Instance.GetProfileItem(item); + if (child == null) + { + continue; + } + ChildItemsObs.Add(child); + } + } + } + + public async Task ChildRemoveAsync() + { + if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + ChildItemsObs.Remove(SelectedChild); + await Task.CompletedTask; + } + + public async Task MoveServer(EMove eMove) + { + if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + var index = ChildItemsObs.IndexOf(SelectedChild); + if (index < 0) + { + return; + } + switch (eMove) + { + case EMove.Top: + if (index == 0) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(0, SelectedChild); + break; + case EMove.Up: + if (index == 0) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(index - 1, SelectedChild); + break; + case EMove.Down: + if (index == ChildItemsObs.Count - 1) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(index + 1, SelectedChild); + break; + case EMove.Bottom: + if (index == ChildItemsObs.Count - 1) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Add(SelectedChild); + break; + default: + break; + } + await Task.CompletedTask; + } + + private async Task SaveServerAsync() + { + var remarks = SelectedSource.Remarks; + if (remarks.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); + return; + } + if (ChildItemsObs.Count == 0) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer); + return; + } + SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); + if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) || + SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup)) + { + return; + } + var childIndexIds = new List(); + foreach (var item in ChildItemsObs) + { + if (item.IndexId.IsNotEmpty()) + { + childIndexIds.Add(item.IndexId); + } + } + SelectedSource.Address = Utils.List2String(childIndexIds); + var profileGroup = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource.IndexId); + profileGroup.ChildItems = Utils.List2String(childIndexIds); + profileGroup.MultipleLoad = PolicyGroupType switch + { + var s when s == ResUI.TbLeastPing => EMultipleLoad.LeastPing, + var s when s == ResUI.TbRandom => EMultipleLoad.Random, + var s when s == ResUI.TbRoundRobin => EMultipleLoad.RoundRobin, + var s when s == ResUI.TbLeastLoad => EMultipleLoad.LeastLoad, + _ => EMultipleLoad.LeastPing, + }; + if (await ConfigHandler.AddGroupServerCommon(_config, SelectedSource, profileGroup, true) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } } diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 83a91ce5..1a180fbd 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -23,6 +23,8 @@ public class MainWindowViewModel : MyReactiveObject public ReactiveCommand AddWireguardServerCmd { get; } public ReactiveCommand AddAnytlsServerCmd { get; } public ReactiveCommand AddCustomServerCmd { get; } + public ReactiveCommand AddPolicyGroupServerCmd { get; } + public ReactiveCommand AddProxyChainServerCmd { get; } public ReactiveCommand AddServerViaClipboardCmd { get; } public ReactiveCommand AddServerViaScanCmd { get; } public ReactiveCommand AddServerViaImageCmd { get; } @@ -122,6 +124,14 @@ public class MainWindowViewModel : MyReactiveObject { await AddServerAsync(true, EConfigType.Custom); }); + AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.PolicyGroup); + }); + AddProxyChainServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.ProxyChain); + }); AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () => { await AddServerViaClipboardAsync(null); @@ -323,6 +333,10 @@ public class MainWindowViewModel : MyReactiveObject { ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); } + else if (eConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + } else { ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 4c0fd2b9..326ea080 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -491,6 +491,10 @@ public class ProfilesViewModel : MyReactiveObject { ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); } + else if (eConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + } else { ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); diff --git a/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml b/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml new file mode 100644 index 00000000..41428b3b --- /dev/null +++ b/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml @@ -0,0 +1,216 @@ + + + + +