diff --git a/v2rayN/v2rayN/App.xaml b/v2rayN/v2rayN/App.xaml index de82e83f..42e6dffc 100644 --- a/v2rayN/v2rayN/App.xaml +++ b/v2rayN/v2rayN/App.xaml @@ -1,9 +1,9 @@  @@ -20,6 +20,7 @@ 13 14 11 + 11 + + + + \ No newline at end of file diff --git a/v2rayN/v2rayN/Common/HttpClientHelper.cs b/v2rayN/v2rayN/Common/HttpClientHelper.cs index 1c338b61..60f9ad2b 100644 --- a/v2rayN/v2rayN/Common/HttpClientHelper.cs +++ b/v2rayN/v2rayN/Common/HttpClientHelper.cs @@ -1,5 +1,6 @@ using System.IO; using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Mime; using System.Text; @@ -21,6 +22,22 @@ namespace v2rayN private HttpClientHelper(HttpClient httpClient) => this.httpClient = httpClient; + public async Task TryGetAsync(string url) + { + if (string.IsNullOrEmpty(url)) + return null; + + try + { + HttpResponseMessage response = await httpClient.GetAsync(url); + return await response.Content.ReadAsStringAsync(); + } + catch + { + return null; + } + } + public async Task GetAsync(string url) { if (Utils.IsNullOrEmpty(url)) return null; @@ -41,6 +58,21 @@ namespace v2rayN var result = await httpClient.PutAsync(url, content); } + public async Task PatchAsync(string url, Dictionary headers) + { + var myContent = JsonUtils.Serialize(headers); + var buffer = System.Text.Encoding.UTF8.GetBytes(myContent); + var byteContent = new ByteArrayContent(buffer); + byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + await httpClient.PatchAsync(url, byteContent); + } + + public async Task DeleteAsync(string url) + { + await httpClient.DeleteAsync(url); + } + public static async Task DownloadFileAsync(HttpClient client, string url, string fileName, IProgress? progress, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(url); diff --git a/v2rayN/v2rayN/Common/YamlUtils.cs b/v2rayN/v2rayN/Common/YamlUtils.cs new file mode 100644 index 00000000..57af2119 --- /dev/null +++ b/v2rayN/v2rayN/Common/YamlUtils.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace v2rayN.Common +{ + internal class YamlUtils + { + #region YAML + + /// + /// 反序列化成对象 + /// + /// + /// + /// + public static T FromYaml(string str) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .Build(); + try + { + T obj = deserializer.Deserialize(str); + return obj; + } + catch (Exception ex) + { + Logging.SaveLog("FromYaml", ex); + return deserializer.Deserialize(""); + } + } + + /// + /// 序列化 + /// + /// + /// + public static string ToYaml(Object obj) + { + var serializer = new SerializerBuilder() + .WithNamingConvention(HyphenatedNamingConvention.Instance) + .Build(); + + string result = string.Empty; + try + { + result = serializer.Serialize(obj); + } + catch (Exception ex) + { + Logging.SaveLog(ex.Message, ex); + } + return result; + } + + #endregion YAML + } +} diff --git a/v2rayN/v2rayN/Enums/ERuleMode.cs b/v2rayN/v2rayN/Enums/ERuleMode.cs new file mode 100644 index 00000000..59a01430 --- /dev/null +++ b/v2rayN/v2rayN/Enums/ERuleMode.cs @@ -0,0 +1,10 @@ +namespace v2rayN.Enums +{ + public enum ERuleMode + { + Rule = 0, + Global = 1, + Direct = 2, + Unchanged = 3 + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/Global.cs b/v2rayN/v2rayN/Global.cs index 067b53ea..17d39a5a 100644 --- a/v2rayN/v2rayN/Global.cs +++ b/v2rayN/v2rayN/Global.cs @@ -183,6 +183,10 @@ namespace v2rayN public static readonly List SingboxMuxs = new() { "h2mux", "smux", "yamux", "" }; public static readonly List TuicCongestionControls = new() { "cubic", "new_reno", "bbr" }; + public static readonly List allowSelectType = new List { "selector", "urltest", "loadbalance", "fallback" }; + public static readonly List notAllowTestType = new List { "selector", "urltest", "direct", "reject", "compatible", "pass", "loadbalance", "fallback" }; + public static readonly List proxyVehicleType = new List { "file", "http" }; + #endregion const } } \ No newline at end of file diff --git a/v2rayN/v2rayN/Handler/ClashApiHandler.cs b/v2rayN/v2rayN/Handler/ClashApiHandler.cs new file mode 100644 index 00000000..45efb8e6 --- /dev/null +++ b/v2rayN/v2rayN/Handler/ClashApiHandler.cs @@ -0,0 +1,219 @@ +using v2rayN.Models; +using static v2rayN.Models.ClashProxies; + +namespace v2rayN.Handler +{ + public sealed class ClashApiHandler + { + private static readonly Lazy instance = new(() => new()); + public static ClashApiHandler Instance => instance.Value; + + private Dictionary _proxies; + public Dictionary ProfileContent { get; set; } + public bool ShowInTaskbar { get; set; } = true; + + public void SetProxies(Dictionary proxies) + { + _proxies = proxies; + } + + public Dictionary GetProxies() + { + return _proxies; + } + + public void GetClashProxies(Config config, Action update) + { + Task.Run(() => GetClashProxiesAsync(config, update)); + } + + private async Task GetClashProxiesAsync(Config config, Action update) + { + for (var i = 0; i < 5; i++) + { + var url = $"{GetApiUrl()}/proxies"; + var result = await HttpClientHelper.Instance.TryGetAsync(url); + var clashProxies = JsonUtils.Deserialize(result); + + var url2 = $"{GetApiUrl()}/providers/proxies"; + var result2 = await HttpClientHelper.Instance.TryGetAsync(url2); + var clashProviders = JsonUtils.Deserialize(result2); + + if (clashProxies != null || clashProviders != null) + { + update(clashProxies, clashProviders); + return; + } + Thread.Sleep(5000); + } + update(null, null); + } + + public void ClashProxiesDelayTest(bool blAll, List lstProxy, Action update) + { + Task.Run(() => + { + if (blAll) + { + for (int i = 0; i < 5; i++) + { + if (GetProxies() != null) + { + break; + } + Thread.Sleep(5000); + } + var proxies = GetProxies(); + if (proxies == null) + { + return; + } + lstProxy = new List(); + foreach (KeyValuePair kv in proxies) + { + if (Global.notAllowTestType.Contains(kv.Value.type.ToLower())) + { + continue; + } + lstProxy.Add(new ClashProxyModel() + { + name = kv.Value.name, + type = kv.Value.type.ToLower(), + }); + } + } + + if (lstProxy == null) + { + return; + } + var urlBase = $"{GetApiUrl()}/proxies"; + urlBase += @"/{0}/delay?timeout=10000&url=" + LazyConfig.Instance.GetConfig().speedTestItem.speedPingTestUrl; + + List tasks = new List(); + foreach (var it in lstProxy) + { + if (Global.notAllowTestType.Contains(it.type.ToLower())) + { + continue; + } + var name = it.name; + var url = string.Format(urlBase, name); + tasks.Add(Task.Run(async () => + { + var result = await HttpClientHelper.Instance.TryGetAsync(url); + update(it, result); + })); + } + Task.WaitAll(tasks.ToArray()); + + Thread.Sleep(1000); + update(null, ""); + }); + } + + public List? GetClashProxyGroups() + { + try + { + var fileContent = ProfileContent; + if (fileContent is null || fileContent?.ContainsKey("proxy-groups") == false) + { + return null; + } + return JsonUtils.Deserialize>(JsonUtils.Serialize(fileContent["proxy-groups"])); + } + catch (Exception ex) + { + Logging.SaveLog("GetClashProxyGroups", ex); + return null; + } + } + + public async void ClashSetActiveProxy(string name, string nameNode) + { + try + { + var url = $"{GetApiUrl()}/proxies/{name}"; + Dictionary headers = new Dictionary(); + headers.Add("name", nameNode); + await HttpClientHelper.Instance.PutAsync(url, headers); + } + catch (Exception ex) + { + Logging.SaveLog(ex.Message, ex); + } + } + + public void ClashConfigUpdate(Dictionary headers) + { + Task.Run(async () => + { + var proxies = GetProxies(); + if (proxies == null) + { + return; + } + + var urlBase = $"{GetApiUrl()}/configs"; + + await HttpClientHelper.Instance.PatchAsync(urlBase, headers); + }); + } + + public async void ClashConfigReload(string filePath) + { + ClashConnectionClose(""); + try + { + var url = $"{GetApiUrl()}/configs?force=true"; + Dictionary headers = new Dictionary(); + headers.Add("path", filePath); + await HttpClientHelper.Instance.PutAsync(url, headers); + } + catch (Exception ex) + { + Logging.SaveLog(ex.Message, ex); + } + } + + public void GetClashConnections(Config config, Action update) + { + Task.Run(() => GetClashConnectionsAsync(config, update)); + } + + private async Task GetClashConnectionsAsync(Config config, Action update) + { + try + { + var url = $"{GetApiUrl()}/connections"; + var result = await HttpClientHelper.Instance.TryGetAsync(url); + var clashConnections = JsonUtils.Deserialize(result); + + update(clashConnections); + } + catch (Exception ex) + { + Logging.SaveLog(ex.Message, ex); + } + } + + public async void ClashConnectionClose(string id) + { + try + { + var url = $"{GetApiUrl()}/connections/{id}"; + await HttpClientHelper.Instance.DeleteAsync(url); + } + catch (Exception ex) + { + Logging.SaveLog(ex.Message, ex); + } + } + + private string GetApiUrl() + { + return $"{Global.HttpProtocol}{Global.Loopback}:{LazyConfig.Instance.StatePort2}"; + } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/Handler/ConfigHandler.cs b/v2rayN/v2rayN/Handler/ConfigHandler.cs index 526c203b..0f789d96 100644 --- a/v2rayN/v2rayN/Handler/ConfigHandler.cs +++ b/v2rayN/v2rayN/Handler/ConfigHandler.cs @@ -194,6 +194,7 @@ namespace v2rayN.Handler down_mbps = 100 }; } + config.clashUIItem ??= new(); LazyConfig.Instance.SetConfig(config); return 0; diff --git a/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigClash.cs b/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigClash.cs new file mode 100644 index 00000000..900c3fd5 --- /dev/null +++ b/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigClash.cs @@ -0,0 +1,259 @@ +using System.IO; +using v2rayN.Common; +using v2rayN.Enums; +using v2rayN.Models; +using v2rayN.Resx; + +namespace v2rayN.Handler.CoreConfig +{ + /// + /// Core configuration file processing class + /// + internal class CoreConfigClash + { + private Config _config; + + public CoreConfigClash(Config config) + { + _config = config; + } + + /// + /// 生成配置文件 + /// + /// + /// + /// + /// + public int GenerateClientConfig(ProfileItem node, string? fileName, out string msg) + { + if (node == null || fileName is null) + { + msg = ResUI.CheckServerSettings; + return -1; + } + + msg = ResUI.InitialConfiguration; + + try + { + if (node == null) + { + msg = ResUI.CheckServerSettings; + return -1; + } + + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + string addressFileName = node.address; + if (string.IsNullOrEmpty(addressFileName)) + { + msg = ResUI.FailedGetDefaultConfiguration; + return -1; + } + if (!File.Exists(addressFileName)) + { + addressFileName = Path.Combine(Utils.GetConfigPath(), addressFileName); + } + if (!File.Exists(addressFileName)) + { + msg = ResUI.FailedReadConfiguration + "1"; + return -1; + } + + string tagYamlStr1 = "!"; + string tagYamlStr2 = "__strn__"; + string tagYamlStr3 = "!!str"; + var txtFile = File.ReadAllText(addressFileName); + txtFile = txtFile.Replace(tagYamlStr1, tagYamlStr2); + + var fileContent = YamlUtils.FromYaml>(txtFile); + if (fileContent == null) + { + msg = ResUI.FailedConversionConfiguration; + return -1; + } + + //port + fileContent["port"] = LazyConfig.Instance.GetLocalPort(EInboundProtocol.http); + //socks-port + fileContent["socks-port"] = LazyConfig.Instance.GetLocalPort(EInboundProtocol.socks); + //log-level + fileContent["log-level"] = _config.coreBasicItem.loglevel; + //external-controller + fileContent["external-controller"] = $"{Global.Loopback}:{LazyConfig.Instance.StatePort2}"; + //allow-lan + if (_config.inbound[0].allowLANConn) + { + fileContent["allow-lan"] = "true"; + fileContent["bind-address"] = "*"; + } + else + { + fileContent["allow-lan"] = "false"; + } + + //ipv6 + //fileContent["ipv6"] = _config.EnableIpv6; + + //mode + if (!fileContent.ContainsKey("mode")) + { + fileContent["mode"] = ERuleMode.Rule.ToString().ToLower(); + } + else + { + if (_config.clashUIItem.ruleMode != ERuleMode.Unchanged) + { + fileContent["mode"] = _config.clashUIItem.ruleMode.ToString().ToLower(); + } + } + + ////enable tun mode + //if (config.EnableTun) + //{ + // string tun = Utils.GetEmbedText(Global.SampleTun); + // if (!string.IsNullOrEmpty(tun)) + // { + // var tunContent = Utils.FromYaml>(tun); + // if (tunContent != null) + // fileContent["tun"] = tunContent["tun"]; + // } + //} + + //Mixin + //try + //{ + // MixinContent(fileContent, config, node); + //} + //catch (Exception ex) + //{ + // Logging.SaveLog("GenerateClientConfigClash-Mixin", ex); + //} + + var txtFileNew = YamlUtils.ToYaml(fileContent).Replace(tagYamlStr2, tagYamlStr3); + File.WriteAllText(fileName, txtFileNew); + //check again + if (!File.Exists(fileName)) + { + msg = ResUI.FailedReadConfiguration + "2"; + return -1; + } + + ClashApiHandler.Instance.ProfileContent = fileContent; + + msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}"); + } + catch (Exception ex) + { + Logging.SaveLog("GenerateClientConfigClash", ex); + msg = ResUI.FailedGenDefaultConfiguration; + return -1; + } + return 0; + } + + //private static void MixinContent(Dictionary fileContent, Config config, ProfileItem node) + //{ + // if (!config.EnableMixinContent) + // { + // return; + // } + + // var path = Utils.GetConfigPath(Global.mixinConfigFileName); + // if (!File.Exists(path)) + // { + // return; + // } + + // var txtFile = File.ReadAllText(Utils.GetConfigPath(Global.mixinConfigFileName)); + // //txtFile = txtFile.Replace("!", ""); + + // var mixinContent = YamlUtils.FromYaml>(txtFile); + // if (mixinContent == null) + // { + // return; + // } + // foreach (var item in mixinContent) + // { + // if (!config.EnableTun && item.Key == "tun") + // { + // continue; + // } + + // if (item.Key.StartsWith("prepend-") + // || item.Key.StartsWith("append-") + // || item.Key.StartsWith("removed-")) + // { + // ModifyContentMerge(fileContent, item.Key, item.Value); + // } + // else + // { + // fileContent[item.Key] = item.Value; + // } + // } + // return; + //} + + private static void ModifyContentMerge(Dictionary fileContent, string key, object value) + { + bool blPrepend = false; + bool blRemoved = false; + if (key.StartsWith("prepend-")) + { + blPrepend = true; + key = key.Replace("prepend-", ""); + } + else if (key.StartsWith("append-")) + { + blPrepend = false; + key = key.Replace("append-", ""); + } + else if (key.StartsWith("removed-")) + { + blRemoved = true; + key = key.Replace("removed-", ""); + } + else + { + return; + } + + if (!blRemoved && !fileContent.ContainsKey(key)) + { + fileContent.Add(key, value); + return; + } + var lstOri = (List)fileContent[key]; + var lstValue = (List)value; + + if (blRemoved) + { + foreach (var item in lstValue) + { + lstOri.RemoveAll(t => t.ToString().StartsWith(item.ToString())); + } + return; + } + + if (blPrepend) + { + lstValue.Reverse(); + foreach (var item in lstValue) + { + lstOri.Insert(0, item); + } + } + else + { + foreach (var item in lstValue) + { + lstOri.Add(item); + } + } + } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigHandler.cs b/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigHandler.cs index cd83394c..daadcff8 100644 --- a/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigHandler.cs +++ b/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigHandler.cs @@ -25,7 +25,15 @@ namespace v2rayN.Handler.CoreConfig msg = ResUI.InitialConfiguration; if (node.configType == EConfigType.Custom) { - return GenerateClientCustomConfig(node, fileName, out msg); + if (node.coreType is ECoreType.clash or ECoreType.clash_meta or ECoreType.mihomo) + { + var configGenClash = new CoreConfigClash(config); + return configGenClash.GenerateClientConfig(node, fileName, out msg); + } + else + { + return GenerateClientCustomConfig(node, fileName, out msg); + } } else if (LazyConfig.Instance.GetCoreType(node, node.configType) == ECoreType.sing_box) { diff --git a/v2rayN/v2rayN/Models/ClashConnectionModel.cs b/v2rayN/v2rayN/Models/ClashConnectionModel.cs new file mode 100644 index 00000000..94911d58 --- /dev/null +++ b/v2rayN/v2rayN/Models/ClashConnectionModel.cs @@ -0,0 +1,17 @@ +namespace v2rayN.Models +{ + public class ClashConnectionModel + { + public string id { get; set; } + public string network { get; set; } + public string type { get; set; } + public string host { get; set; } + public ulong upload { get; set; } + public ulong download { get; set; } + public string uploadTraffic { get; set; } + public string downloadTraffic { get; set; } + public double time { get; set; } + public string elapsed { get; set; } + public string chain { get; set; } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/Models/ClashConnections.cs b/v2rayN/v2rayN/Models/ClashConnections.cs new file mode 100644 index 00000000..439e3944 --- /dev/null +++ b/v2rayN/v2rayN/Models/ClashConnections.cs @@ -0,0 +1,37 @@ +namespace v2rayN.Models +{ + public class ClashConnections + { + public ulong downloadTotal { get; set; } + public ulong uploadTotal { get; set; } + public List? connections { get; set; } + } + + public class ConnectionItem + { + public string id { get; set; } = string.Empty; + public MetadataItem metadata { get; set; } + public ulong upload { get; set; } + public ulong download { get; set; } + public DateTime start { get; set; } + public List? chains { get; set; } + public string rule { get; set; } + public string rulePayload { get; set; } + } + + public class MetadataItem + { + public string network { get; set; } + public string type { get; set; } + public string sourceIP { get; set; } + public string destinationIP { get; set; } + public string sourcePort { get; set; } + public string destinationPort { get; set; } + public string host { get; set; } + public string nsMode { get; set; } + public object uid { get; set; } + public string process { get; set; } + public string processPath { get; set; } + public string remoteDestination { get; set; } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/Models/ClashProviders.cs b/v2rayN/v2rayN/Models/ClashProviders.cs new file mode 100644 index 00000000..e59ac58d --- /dev/null +++ b/v2rayN/v2rayN/Models/ClashProviders.cs @@ -0,0 +1,19 @@ + + +using static v2rayN.Models.ClashProxies; + +namespace v2rayN.Models +{ + public class ClashProviders + { + public Dictionary providers { get; set; } + + public class ProvidersItem + { + public string name { get; set; } + public ProxiesItem[] proxies { get; set; } + public string type { get; set; } + public string vehicleType { get; set; } + } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/Models/ClashProxies.cs b/v2rayN/v2rayN/Models/ClashProxies.cs new file mode 100644 index 00000000..4e8560c8 --- /dev/null +++ b/v2rayN/v2rayN/Models/ClashProxies.cs @@ -0,0 +1,24 @@ +namespace v2rayN.Models +{ + public class ClashProxies + { + public Dictionary proxies { get; set; } + + public class ProxiesItem + { + public string[] all { get; set; } + public List history { get; set; } + public string name { get; set; } + public string type { get; set; } + public bool udp { get; set; } + public string now { get; set; } + public int delay { get; set; } + } + + public class HistoryItem + { + public string time { get; set; } + public int delay { get; set; } + } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/Models/ClashProxyModel.cs b/v2rayN/v2rayN/Models/ClashProxyModel.cs new file mode 100644 index 00000000..13baeb1c --- /dev/null +++ b/v2rayN/v2rayN/Models/ClashProxyModel.cs @@ -0,0 +1,26 @@ +using ReactiveUI.Fody.Helpers; + +namespace v2rayN.Models +{ + [Serializable] + public class ClashProxyModel + { + [Reactive] + public string name { get; set; } + + [Reactive] + public string type { get; set; } + + [Reactive] + public string now { get; set; } + + [Reactive] + public int delay { get; set; } + + [Reactive] + public string delayName { get; set; } + + [Reactive] + public bool isActive { get; set; } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/Models/Config.cs b/v2rayN/v2rayN/Models/Config.cs index 0d411d66..ea921008 100644 --- a/v2rayN/v2rayN/Models/Config.cs +++ b/v2rayN/v2rayN/Models/Config.cs @@ -33,6 +33,7 @@ namespace v2rayN.Models public SpeedTestItem speedTestItem { get; set; } public Mux4SboxItem mux4SboxItem { get; set; } public HysteriaItem hysteriaItem { get; set; } + public ClashUIItem clashUIItem { get; set; } public List inbound { get; set; } public List globalHotkeys { get; set; } public List coreTypeItem { get; set; } diff --git a/v2rayN/v2rayN/Models/ConfigItems.cs b/v2rayN/v2rayN/Models/ConfigItems.cs index a986dbf9..5e4fd795 100644 --- a/v2rayN/v2rayN/Models/ConfigItems.cs +++ b/v2rayN/v2rayN/Models/ConfigItems.cs @@ -211,4 +211,16 @@ namespace v2rayN.Models public int up_mbps { get; set; } public int down_mbps { get; set; } } + + [Serializable] + public class ClashUIItem + { + public ERuleMode ruleMode { get; set; } + public int proxiesSorting { get; set; } + public bool proxiesAutoRefresh { get; set; } + public int AutoDelayTestInterval { get; set; } = 10; + public int connectionsSorting { get; set; } + public bool connectionsAutoRefresh { get; set; } + + } } \ No newline at end of file diff --git a/v2rayN/v2rayN/Resx/ResUI.Designer.cs b/v2rayN/v2rayN/Resx/ResUI.Designer.cs index 52099eff..3ae54b09 100644 --- a/v2rayN/v2rayN/Resx/ResUI.Designer.cs +++ b/v2rayN/v2rayN/Resx/ResUI.Designer.cs @@ -753,6 +753,24 @@ namespace v2rayN.Resx { } } + /// + /// 查找类似 Close Connection 的本地化字符串。 + /// + public static string menuConnectionClose { + get { + return ResourceManager.GetString("menuConnectionClose", resourceCulture); + } + } + + /// + /// 查找类似 Close All Connection 的本地化字符串。 + /// + public static string menuConnectionCloseAll { + get { + return ResourceManager.GetString("menuConnectionCloseAll", resourceCulture); + } + } + /// /// 查找类似 Clone selected server 的本地化字符串。 /// @@ -879,6 +897,42 @@ namespace v2rayN.Resx { } } + /// + /// 查找类似 Direct 的本地化字符串。 + /// + public static string menuModeDirect { + get { + return ResourceManager.GetString("menuModeDirect", resourceCulture); + } + } + + /// + /// 查找类似 Global 的本地化字符串。 + /// + public static string menuModeGlobal { + get { + return ResourceManager.GetString("menuModeGlobal", resourceCulture); + } + } + + /// + /// 查找类似 Do not change 的本地化字符串。 + /// + public static string menuModeNothing { + get { + return ResourceManager.GetString("menuModeNothing", resourceCulture); + } + } + + /// + /// 查找类似 Rule 的本地化字符串。 + /// + public static string menuModeRule { + get { + return ResourceManager.GetString("menuModeRule", resourceCulture); + } + } + /// /// 查找类似 Move to bottom (B) 的本地化字符串。 /// @@ -1005,6 +1059,42 @@ namespace v2rayN.Resx { } } + /// + /// 查找类似 All Node Latency Test 的本地化字符串。 + /// + public static string menuProxiesDelaytest { + get { + return ResourceManager.GetString("menuProxiesDelaytest", resourceCulture); + } + } + + /// + /// 查找类似 Part Node Latency Test 的本地化字符串。 + /// + public static string menuProxiesDelaytestPart { + get { + return ResourceManager.GetString("menuProxiesDelaytestPart", resourceCulture); + } + } + + /// + /// 查找类似 Refresh Proxies (F5) 的本地化字符串。 + /// + public static string menuProxiesReload { + get { + return ResourceManager.GetString("menuProxiesReload", resourceCulture); + } + } + + /// + /// 查找类似 Select active node (Enter) 的本地化字符串。 + /// + public static string menuProxiesSelectActivity { + get { + return ResourceManager.GetString("menuProxiesSelectActivity", resourceCulture); + } + } + /// /// 查找类似 Test servers real delay (Ctrl+R) 的本地化字符串。 /// @@ -1176,6 +1266,15 @@ namespace v2rayN.Resx { } } + /// + /// 查找类似 Rule mode 的本地化字符串。 + /// + public static string menuRulemode { + get { + return ResourceManager.GetString("menuRulemode", resourceCulture); + } + } + /// /// 查找类似 Remove Rule (Delete) 的本地化字符串。 /// @@ -1996,6 +2095,15 @@ namespace v2rayN.Resx { } } + /// + /// 查找类似 Connections 的本地化字符串。 + /// + public static string TbConnections { + get { + return ResourceManager.GetString("TbConnections", resourceCulture); + } + } + /// /// 查找类似 Core Type 的本地化字符串。 /// @@ -2266,6 +2374,15 @@ namespace v2rayN.Resx { } } + /// + /// 查找类似 Proxies 的本地化字符串。 + /// + public static string TbProxies { + get { + return ResourceManager.GetString("TbProxies", resourceCulture); + } + } + /// /// 查找类似 PublicKey 的本地化字符串。 /// @@ -3139,6 +3256,123 @@ namespace v2rayN.Resx { } } + /// + /// 查找类似 Sorting 的本地化字符串。 + /// + public static string TbSorting { + get { + return ResourceManager.GetString("TbSorting", resourceCulture); + } + } + + /// + /// 查找类似 Chain 的本地化字符串。 + /// + public static string TbSortingChain { + get { + return ResourceManager.GetString("TbSortingChain", resourceCulture); + } + } + + /// + /// 查找类似 Default 的本地化字符串。 + /// + public static string TbSortingDefault { + get { + return ResourceManager.GetString("TbSortingDefault", resourceCulture); + } + } + + /// + /// 查找类似 Delay 的本地化字符串。 + /// + public static string TbSortingDelay { + get { + return ResourceManager.GetString("TbSortingDelay", resourceCulture); + } + } + + /// + /// 查找类似 Download Speed 的本地化字符串。 + /// + public static string TbSortingDownSpeed { + get { + return ResourceManager.GetString("TbSortingDownSpeed", resourceCulture); + } + } + + /// + /// 查找类似 Download Traffic 的本地化字符串。 + /// + public static string TbSortingDownTraffic { + get { + return ResourceManager.GetString("TbSortingDownTraffic", resourceCulture); + } + } + + /// + /// 查找类似 Host 的本地化字符串。 + /// + public static string TbSortingHost { + get { + return ResourceManager.GetString("TbSortingHost", resourceCulture); + } + } + + /// + /// 查找类似 Name 的本地化字符串。 + /// + public static string TbSortingName { + get { + return ResourceManager.GetString("TbSortingName", resourceCulture); + } + } + + /// + /// 查找类似 Network 的本地化字符串。 + /// + public static string TbSortingNetwork { + get { + return ResourceManager.GetString("TbSortingNetwork", resourceCulture); + } + } + + /// + /// 查找类似 Time 的本地化字符串。 + /// + public static string TbSortingTime { + get { + return ResourceManager.GetString("TbSortingTime", resourceCulture); + } + } + + /// + /// 查找类似 Type 的本地化字符串。 + /// + public static string TbSortingType { + get { + return ResourceManager.GetString("TbSortingType", resourceCulture); + } + } + + /// + /// 查找类似 Upload Speed 的本地化字符串。 + /// + public static string TbSortingUpSpeed { + get { + return ResourceManager.GetString("TbSortingUpSpeed", resourceCulture); + } + } + + /// + /// 查找类似 Upload Traffic 的本地化字符串。 + /// + public static string TbSortingUpTraffic { + get { + return ResourceManager.GetString("TbSortingUpTraffic", resourceCulture); + } + } + /// /// 查找类似 SpiderX 的本地化字符串。 /// diff --git a/v2rayN/v2rayN/Resx/ResUI.fa-Ir.resx b/v2rayN/v2rayN/Resx/ResUI.fa-Ir.resx index f539cd2b..dd9b5d9d 100644 --- a/v2rayN/v2rayN/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/v2rayN/Resx/ResUI.fa-Ir.resx @@ -1006,4 +1006,70 @@ فعال کردن کش فایل مجموعه قوانین برای sing-box + + مرتب سازی + + + Default + + + تاخیر + + + سرعت دانلود + + + ترافیک دانلود + + + نام + + + زمان + + + سرعت اپلود + + + ترافیک آپلود + + + اتصالات + + + بستن اتصال + + + تمام اتصالات را ببندید + + + پروکسی + + + نوع قانون + + + Direct + + + Global + + + تغییر نده + + + قانون + + + All Node Latency Test + + + Part Node Latency Test + + + Refresh Proxies (F5) + + + Select active node (Enter) + \ No newline at end of file diff --git a/v2rayN/v2rayN/Resx/ResUI.resx b/v2rayN/v2rayN/Resx/ResUI.resx index 4dfd5108..bdd0ea8e 100644 --- a/v2rayN/v2rayN/Resx/ResUI.resx +++ b/v2rayN/v2rayN/Resx/ResUI.resx @@ -1219,4 +1219,82 @@ Open the storage location + + Sorting + + + Chain + + + Default + + + Delay + + + Download Speed + + + Download Traffic + + + Host + + + Name + + + Network + + + Time + + + Type + + + Upload Speed + + + Upload Traffic + + + Connections + + + Close Connection + + + Close All Connection + + + Proxies + + + Rule mode + + + Direct + + + Global + + + Do not change + + + Rule + + + All Node Latency Test + + + Part Node Latency Test + + + Refresh Proxies (F5) + + + Select active node (Enter) + \ No newline at end of file diff --git a/v2rayN/v2rayN/Resx/ResUI.zh-Hans.resx b/v2rayN/v2rayN/Resx/ResUI.zh-Hans.resx index 4a46b98e..efcd4f01 100644 --- a/v2rayN/v2rayN/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/v2rayN/Resx/ResUI.zh-Hans.resx @@ -1216,4 +1216,82 @@ 打开存储所在的位置 + + 排序 + + + 路由链 + + + 默认 + + + 延迟 + + + 下载速度 + + + 下载流量 + + + 主机 + + + 名称 + + + 网络 + + + 时间 + + + 类型 + + + 上传速度 + + + 上传流量 + + + 当前连接 + + + 关闭连接 + + + 关闭所有连接 + + + 当前代理 + + + 规则模式 + + + 直连 + + + 全局 + + + 随原配置 + + + 规则 + + + 全部节点延迟测试 + + + 当前部分节点延迟测试 + + + 刷新 (F5) + + + 设为活动节点 (Enter) + \ No newline at end of file diff --git a/v2rayN/v2rayN/ViewModels/ClashConnectionsViewModel.cs b/v2rayN/v2rayN/ViewModels/ClashConnectionsViewModel.cs new file mode 100644 index 00000000..046c8c50 --- /dev/null +++ b/v2rayN/v2rayN/ViewModels/ClashConnectionsViewModel.cs @@ -0,0 +1,195 @@ +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; +using System.Reactive; +using System.Reactive.Linq; +using System.Windows; +using v2rayN.Handler; +using v2rayN.Models; + +namespace v2rayN.ViewModels +{ + public class ClashConnectionsViewModel : ReactiveObject + { + private static Config _config; + + static ClashConnectionsViewModel() + { + _config = LazyConfig.Instance.GetConfig(); + } + + private IObservableCollection _connectionItems = new ObservableCollectionExtended(); + + public IObservableCollection ConnectionItems => _connectionItems; + + [Reactive] + public ClashConnectionModel SelectedSource { get; set; } + + public ReactiveCommand ConnectionCloseCmd { get; } + public ReactiveCommand ConnectionCloseAllCmd { get; } + + [Reactive] + public int SortingSelected { get; set; } + + [Reactive] + public bool AutoRefresh { get; set; } + + private int AutoRefreshInterval; + + public ClashConnectionsViewModel() + { + AutoRefreshInterval = 10; + SortingSelected = _config.clashUIItem.connectionsSorting; + AutoRefresh = _config.clashUIItem.connectionsAutoRefresh; + + var canEditRemove = this.WhenAnyValue( + x => x.SelectedSource, + selectedSource => selectedSource != null && !string.IsNullOrEmpty(selectedSource.id)); + + this.WhenAnyValue( + x => x.SortingSelected, + y => y >= 0) + .Subscribe(c => DoSortingSelected(c)); + + this.WhenAnyValue( + x => x.AutoRefresh, + y => y == true) + .Subscribe(c => { _config.clashUIItem.connectionsAutoRefresh = AutoRefresh; }); + + ConnectionCloseCmd = ReactiveCommand.Create(() => + { + ClashConnectionClose(false); + }, canEditRemove); + + ConnectionCloseAllCmd = ReactiveCommand.Create(() => + { + ClashConnectionClose(true); + }); + + Init(); + } + + private void DoSortingSelected(bool c) + { + if (!c) + { + return; + } + if (SortingSelected != _config.clashUIItem.connectionsSorting) + { + _config.clashUIItem.connectionsSorting = SortingSelected; + } + + GetClashConnections(); + } + + private void Init() + { + Observable.Interval(TimeSpan.FromSeconds(AutoRefreshInterval)) + .Subscribe(x => + { + if (!(AutoRefresh && ClashApiHandler.Instance.ShowInTaskbar)) + { + return; + } + GetClashConnections(); + }); + } + + private void GetClashConnections() + { + ClashApiHandler.Instance.GetClashConnections(_config, (it) => + { + if (it == null) + { + return; + } + + Application.Current?.Dispatcher.Invoke((Action)(() => + { + RefreshConnections(it?.connections); + })); + }); + } + + private void RefreshConnections(List? connections) + { + _connectionItems.Clear(); + + var dtNow = DateTime.Now; + var lstModel = new List(); + foreach (var item in connections ?? []) + { + ClashConnectionModel model = new(); + + model.id = item.id; + model.network = item.metadata.network; + model.type = item.metadata.type; + model.host = $"{(string.IsNullOrEmpty(item.metadata.host) ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}"; + var sp = (dtNow - item.start); + model.time = sp.TotalSeconds < 0 ? 1 : sp.TotalSeconds; + model.upload = item.upload; + model.download = item.download; + model.uploadTraffic = $"{Utils.HumanFy((long)item.upload)}"; + model.downloadTraffic = $"{Utils.HumanFy((long)item.download)}"; + model.elapsed = sp.ToString(@"hh\:mm\:ss"); + model.chain = item.chains?.Count > 0 ? item.chains[0] : String.Empty; + + lstModel.Add(model); + } + if (lstModel.Count <= 0) { return; } + + //sort + switch (SortingSelected) + { + case 0: + lstModel = lstModel.OrderBy(t => t.upload / t.time).ToList(); + break; + + case 1: + lstModel = lstModel.OrderBy(t => t.download / t.time).ToList(); + break; + + case 2: + lstModel = lstModel.OrderBy(t => t.upload).ToList(); + break; + + case 3: + lstModel = lstModel.OrderBy(t => t.download).ToList(); + break; + + case 4: + lstModel = lstModel.OrderBy(t => t.time).ToList(); + break; + + case 5: + lstModel = lstModel.OrderBy(t => t.host).ToList(); + break; + } + + _connectionItems.AddRange(lstModel); + } + + public void ClashConnectionClose(bool all) + { + var id = string.Empty; + if (!all) + { + var item = SelectedSource; + if (item is null) + { + return; + } + id = item.id; + } + else + { + _connectionItems.Clear(); + } + ClashApiHandler.Instance.ClashConnectionClose(id); + GetClashConnections(); + } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/ViewModels/ClashProxiesViewModel.cs b/v2rayN/v2rayN/ViewModels/ClashProxiesViewModel.cs new file mode 100644 index 00000000..2813c742 --- /dev/null +++ b/v2rayN/v2rayN/ViewModels/ClashProxiesViewModel.cs @@ -0,0 +1,487 @@ +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; +using System.Reactive; +using System.Reactive.Linq; +using System.Windows; +using v2rayN.Enums; +using v2rayN.Handler; +using v2rayN.Models; +using v2rayN.Resx; +using static v2rayN.Models.ClashProviders; +using static v2rayN.Models.ClashProxies; + +namespace v2rayN.ViewModels +{ + public class ClashProxiesViewModel : ReactiveObject + { + private static Config _config; + private NoticeHandler? _noticeHandler; + private Dictionary? proxies; + private Dictionary? providers; + private int delayTimeout = 99999999; + + private IObservableCollection _proxyGroups = new ObservableCollectionExtended(); + private IObservableCollection _proxyDetails = new ObservableCollectionExtended(); + + public IObservableCollection ProxyGroups => _proxyGroups; + public IObservableCollection ProxyDetails => _proxyDetails; + + [Reactive] + public ClashProxyModel SelectedGroup { get; set; } + + [Reactive] + public ClashProxyModel SelectedDetail { get; set; } + + public ReactiveCommand ProxiesReloadCmd { get; } + public ReactiveCommand ProxiesDelaytestCmd { get; } + public ReactiveCommand ProxiesDelaytestPartCmd { get; } + public ReactiveCommand ProxiesSelectActivityCmd { get; } + + [Reactive] + public int RuleModeSelected { get; set; } + + [Reactive] + public int SortingSelected { get; set; } + + [Reactive] + public bool AutoRefresh { get; set; } + + public ClashProxiesViewModel() + { + _noticeHandler = Locator.Current.GetService(); + _config = LazyConfig.Instance.GetConfig(); + + SelectedGroup = new(); + SelectedDetail = new(); + + AutoRefresh = _config.clashUIItem.proxiesAutoRefresh; + SortingSelected = _config.clashUIItem.proxiesSorting; + RuleModeSelected = (int)_config.clashUIItem.ruleMode; + + this.WhenAnyValue( + x => x.SelectedGroup, + y => y != null && !string.IsNullOrEmpty(y.name)) + .Subscribe(c => RefreshProxyDetails(c)); + + this.WhenAnyValue( + x => x.RuleModeSelected, + y => y >= 0) + .Subscribe(c => DoRulemodeSelected(c)); + + this.WhenAnyValue( + x => x.SortingSelected, + y => y >= 0) + .Subscribe(c => DoSortingSelected(c)); + + this.WhenAnyValue( + x => x.AutoRefresh, + y => y == true) + .Subscribe(c => { _config.clashUIItem.proxiesAutoRefresh = AutoRefresh; }); + + ProxiesReloadCmd = ReactiveCommand.Create(() => + { + ProxiesReload(); + }); + ProxiesDelaytestCmd = ReactiveCommand.Create(() => + { + ProxiesDelayTest(true); + }); + + ProxiesDelaytestPartCmd = ReactiveCommand.Create(() => + { + ProxiesDelayTest(false); + }); + ProxiesSelectActivityCmd = ReactiveCommand.Create(() => + { + SetActiveProxy(); + }); + + ProxiesReload(); + DelayTestTask(); + } + + private void DoRulemodeSelected(bool c) + { + if (!c) + { + return; + } + if (_config.clashUIItem.ruleMode == (ERuleMode)RuleModeSelected) + { + return; + } + SetRuleModeCheck((ERuleMode)RuleModeSelected); + } + + public void SetRuleModeCheck(ERuleMode mode) + { + if (_config.clashUIItem.ruleMode == mode) + { + return; + } + SetRuleMode(mode); + } + + private void DoSortingSelected(bool c) + { + if (!c) + { + return; + } + if (SortingSelected != _config.clashUIItem.proxiesSorting) + { + _config.clashUIItem.proxiesSorting = SortingSelected; + } + + RefreshProxyDetails(c); + } + + private void UpdateHandler(bool notify, string msg) + { + _noticeHandler?.SendMessage(msg, true); + } + + public void ProxiesReload() + { + GetClashProxies(true); + } + + public void ProxiesClear() + { + proxies = null; + providers = null; + + ClashApiHandler.Instance.SetProxies(proxies); + + Application.Current?.Dispatcher.Invoke((Action)(() => + { + _proxyGroups.Clear(); + _proxyDetails.Clear(); + })); + } + + public void ProxiesDelayTest() + { + ProxiesDelayTest(true); + } + + #region proxy function + + private void SetRuleMode(ERuleMode mode) + { + _config.clashUIItem.ruleMode = mode; + + if (mode != ERuleMode.Unchanged) + { + Dictionary headers = new Dictionary(); + headers.Add("mode", mode.ToString().ToLower()); + ClashApiHandler.Instance.ClashConfigUpdate(headers); + } + } + + private void GetClashProxies(bool refreshUI) + { + ClashApiHandler.Instance.GetClashProxies(_config, (it, it2) => + { + UpdateHandler(false, "Refresh Clash Proxies"); + proxies = it?.proxies; + providers = it2?.providers; + + ClashApiHandler.Instance.SetProxies(proxies); + if (proxies == null) + { + return; + } + if (refreshUI) + { + Application.Current?.Dispatcher.Invoke((Action)(() => + { + RefreshProxyGroups(); + })); + } + }); + } + + private void RefreshProxyGroups() + { + var selectedName = SelectedGroup?.name; + _proxyGroups.Clear(); + + var proxyGroups = ClashApiHandler.Instance.GetClashProxyGroups(); + if (proxyGroups != null && proxyGroups.Count > 0) + { + foreach (var it in proxyGroups) + { + if (string.IsNullOrEmpty(it.name) || !proxies.ContainsKey(it.name)) + { + continue; + } + var item = proxies[it.name]; + if (!Global.allowSelectType.Contains(item.type.ToLower())) + { + continue; + } + _proxyGroups.Add(new ClashProxyModel() + { + now = item.now, + name = item.name, + type = item.type + }); + } + } + + //from api + foreach (KeyValuePair kv in proxies) + { + if (!Global.allowSelectType.Contains(kv.Value.type.ToLower())) + { + continue; + } + var item = _proxyGroups.Where(t => t.name == kv.Key).FirstOrDefault(); + if (item != null && !string.IsNullOrEmpty(item.name)) + { + continue; + } + _proxyGroups.Add(new ClashProxyModel() + { + now = kv.Value.now, + name = kv.Key, + type = kv.Value.type + }); + } + + if (_proxyGroups != null && _proxyGroups.Count > 0) + { + if (selectedName != null && _proxyGroups.Any(t => t.name == selectedName)) + { + SelectedGroup = _proxyGroups.FirstOrDefault(t => t.name == selectedName); + } + else + { + SelectedGroup = _proxyGroups[0]; + } + } + else + { + SelectedGroup = new(); + } + } + + private void RefreshProxyDetails(bool c) + { + _proxyDetails.Clear(); + if (!c) + { + return; + } + var name = SelectedGroup?.name; + if (string.IsNullOrEmpty(name)) + { + return; + } + if (proxies == null) + { + return; + } + + proxies.TryGetValue(name, out ProxiesItem proxy); + if (proxy == null || proxy.all == null) + { + return; + } + var lstDetails = new List(); + foreach (var item in proxy.all) + { + var isActive = item == proxy.now; + + var proxy2 = TryGetProxy(item); + if (proxy2 == null) + { + continue; + } + int delay = -1; + if (proxy2.history.Count > 0) + { + delay = proxy2.history[proxy2.history.Count - 1].delay; + } + + lstDetails.Add(new ClashProxyModel() + { + isActive = isActive, + name = item, + type = proxy2.type, + delay = delay <= 0 ? delayTimeout : delay, + delayName = delay <= 0 ? string.Empty : $"{delay}ms", + }); + } + //sort + switch (SortingSelected) + { + case 0: + lstDetails = lstDetails.OrderBy(t => t.delay).ToList(); + break; + + case 1: + lstDetails = lstDetails.OrderBy(t => t.name).ToList(); + break; + + default: + break; + } + _proxyDetails.AddRange(lstDetails); + } + + private ProxiesItem? TryGetProxy(string name) + { + if(proxies is null) + return null; + proxies.TryGetValue(name, out ProxiesItem proxy2); + if (proxy2 != null) + { + return proxy2; + } + //from providers + if (providers != null) + { + foreach (KeyValuePair kv in providers) + { + if (Global.proxyVehicleType.Contains(kv.Value.vehicleType.ToLower())) + { + var proxy3 = kv.Value.proxies.FirstOrDefault(t => t.name == name); + if (proxy3 != null) + { + return proxy3; + } + } + } + } + return null; + } + + public void SetActiveProxy() + { + if (SelectedGroup == null || string.IsNullOrEmpty(SelectedGroup.name)) + { + return; + } + if (SelectedDetail == null || string.IsNullOrEmpty(SelectedDetail.name)) + { + return; + } + var name = SelectedGroup.name; + if (string.IsNullOrEmpty(name)) + { + return; + } + var nameNode = SelectedDetail.name; + if (string.IsNullOrEmpty(nameNode)) + { + return; + } + var selectedProxy = TryGetProxy(name); + if (selectedProxy == null || selectedProxy.type != "Selector") + { + _noticeHandler?.Enqueue(ResUI.OperationFailed); + return; + } + + ClashApiHandler.Instance.ClashSetActiveProxy(name, nameNode); + + selectedProxy.now = nameNode; + var group = _proxyGroups.Where(it => it.name == SelectedGroup.name).FirstOrDefault(); + if (group != null) + { + group.now = nameNode; + var group2 = JsonUtils.DeepCopy(group); + _proxyGroups.Replace(group, group2); + + SelectedGroup = group2; + + //var index = _proxyGroups.IndexOf(group); + //_proxyGroups.Remove(group); + //_proxyGroups.Insert(index, group); + } + _noticeHandler?.Enqueue(ResUI.OperationSuccess); + + //RefreshProxyDetails(true); + //GetClashProxies(true); + } + + private void ProxiesDelayTest(bool blAll) + { + UpdateHandler(false, "Clash Proxies Latency Test"); + + ClashApiHandler.Instance.ClashProxiesDelayTest(blAll, _proxyDetails.ToList(), (item, result) => + { + if (item == null) + { + GetClashProxies(true); + return; + } + if (string.IsNullOrEmpty(result)) + { + return; + } + Application.Current?.Dispatcher.Invoke((Action)(() => + { + //UpdateHandler(false, $"{item.name}={result}"); + var detail = _proxyDetails.Where(it => it.name == item.name).FirstOrDefault(); + if (detail != null) + { + var dicResult = JsonUtils.Deserialize>(result); + if (dicResult != null && dicResult.ContainsKey("delay")) + { + detail.delay = Convert.ToInt32(dicResult["delay"]); + detail.delayName = $"{dicResult["delay"]}ms"; + } + else if (dicResult != null && dicResult.ContainsKey("message")) + { + detail.delay = delayTimeout; + detail.delayName = $"{dicResult["message"]}"; + } + else + { + detail.delay = delayTimeout; + detail.delayName = String.Empty; + } + _proxyDetails.Replace(detail, JsonUtils.DeepCopy(detail)); + } + })); + }); + } + + #endregion proxy function + + #region task + + public void DelayTestTask() + { + var autoDelayTestTime = DateTime.Now; + + Observable.Interval(TimeSpan.FromSeconds(60)) + .Subscribe(x => + { + if (!(AutoRefresh && ClashApiHandler.Instance.ShowInTaskbar)) + { + return; + } + var dtNow = DateTime.Now; + + if (_config.clashUIItem.AutoDelayTestInterval > 0) + { + if ((dtNow - autoDelayTestTime).Minutes % _config.clashUIItem.AutoDelayTestInterval == 0) + { + ProxiesDelayTest(); + autoDelayTestTime = dtNow; + } + Thread.Sleep(1000); + } + }); + } + + #endregion task + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/ViewModels/MainWindowViewModel.cs b/v2rayN/v2rayN/ViewModels/MainWindowViewModel.cs index f7cd4585..57f522ef 100644 --- a/v2rayN/v2rayN/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/v2rayN/ViewModels/MainWindowViewModel.cs @@ -1502,7 +1502,7 @@ namespace v2rayN.ViewModels Application.Current?.Dispatcher.Invoke((Action)(() => { BlReloadEnabled = true; - })); + })); }); } @@ -1515,7 +1515,7 @@ namespace v2rayN.ViewModels //ConfigHandler.SaveConfig(_config, false); ChangeSystemProxyStatus(_config.sysProxyType, false); - }); + }); } private void CloseCore() @@ -1775,6 +1775,7 @@ namespace v2rayN.ViewModels Application.Current.Resources["StdFontSize1"] = size + 1; Application.Current.Resources["StdFontSize2"] = size + 2; Application.Current.Resources["StdFontSizeMsg"] = size - 1; + Application.Current.Resources["StdFontSize-1"] = size - 1; ConfigHandler.SaveConfig(_config); } diff --git a/v2rayN/v2rayN/Views/ClashConnectionsView.xaml b/v2rayN/v2rayN/Views/ClashConnectionsView.xaml new file mode 100644 index 00000000..97168ab4 --- /dev/null +++ b/v2rayN/v2rayN/Views/ClashConnectionsView.xaml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs b/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs new file mode 100644 index 00000000..b91161e6 --- /dev/null +++ b/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs @@ -0,0 +1,37 @@ +using v2rayN.ViewModels; +using ReactiveUI; +using System.Reactive.Disposables; + +namespace v2rayN.Views +{ + /// + /// Interaction logic for ConnectionsView.xaml + /// + public partial class ClashConnectionsView + { + public ClashConnectionsView() + { + InitializeComponent(); + ViewModel = new ClashConnectionsViewModel(); + + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.ConnectionItems, v => v.lstConnections.ItemsSource).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource, v => v.lstConnections.SelectedItem).DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.ConnectionItems.Count, v => v.chipCount.Content).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.ConnectionCloseCmd, v => v.menuConnectionClose).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.menuConnectionCloseAll).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.SortingSelected, v => v.cmbSorting.SelectedIndex).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.btnConnectionCloseAll).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); + }); + } + + private void btnClose_Click(object sender, System.Windows.RoutedEventArgs e) + { + ViewModel?.ClashConnectionClose(false); + } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/Views/ClashProxiesView.xaml b/v2rayN/v2rayN/Views/ClashProxiesView.xaml new file mode 100644 index 00000000..27748486 --- /dev/null +++ b/v2rayN/v2rayN/Views/ClashProxiesView.xaml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/v2rayN/v2rayN/Views/ClashProxiesView.xaml.cs b/v2rayN/v2rayN/Views/ClashProxiesView.xaml.cs new file mode 100644 index 00000000..ace999ac --- /dev/null +++ b/v2rayN/v2rayN/Views/ClashProxiesView.xaml.cs @@ -0,0 +1,60 @@ +using v2rayN.ViewModels; +using ReactiveUI; +using Splat; +using System.Reactive.Disposables; +using System.Windows.Input; + +namespace v2rayN.Views +{ + /// + /// Interaction logic for ProxiesView.xaml + /// + public partial class ClashProxiesView + { + public ClashProxiesView() + { + InitializeComponent(); + ViewModel = new ClashProxiesViewModel(); + Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ClashProxiesViewModel)); + lstProxyDetails.PreviewMouseDoubleClick += lstProxyDetails_PreviewMouseDoubleClick; + + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.ProxyGroups, v => v.lstProxyGroups.ItemsSource).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedGroup, v => v.lstProxyGroups.SelectedItem).DisposeWith(disposables); + + this.OneWayBind(ViewModel, vm => vm.ProxyDetails, v => v.lstProxyDetails.ItemsSource).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedDetail, v => v.lstProxyDetails.SelectedItem).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.ProxiesReloadCmd, v => v.menuProxiesReload).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.ProxiesDelaytestCmd, v => v.menuProxiesDelaytest).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.ProxiesDelaytestPartCmd, v => v.menuProxiesDelaytestPart).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.ProxiesSelectActivityCmd, v => v.menuProxiesSelectActivity).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.RuleModeSelected, v => v.cmbRulemode.SelectedIndex).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SortingSelected, v => v.cmbSorting.SelectedIndex).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); + }); + } + + private void ProxiesView_KeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.F5: + ViewModel?.ProxiesReload(); + break; + + case Key.Enter: + ViewModel?.SetActiveProxy(); + break; + } + } + + private void lstProxyDetails_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e) + { + ViewModel?.SetActiveProxy(); + } + } +} \ No newline at end of file diff --git a/v2rayN/v2rayN/v2rayN.csproj b/v2rayN/v2rayN/v2rayN.csproj index 9db203a5..c92f8cd6 100644 --- a/v2rayN/v2rayN/v2rayN.csproj +++ b/v2rayN/v2rayN/v2rayN.csproj @@ -26,6 +26,7 @@ +