diff --git a/v2rayN/ServiceLib/Common/CountryExtension.cs b/v2rayN/ServiceLib/Common/CountryExtension.cs new file mode 100644 index 00000000..152172d8 --- /dev/null +++ b/v2rayN/ServiceLib/Common/CountryExtension.cs @@ -0,0 +1,92 @@ +namespace ServiceLib.Common; + +/// +/// Extension methods for country code utilities +/// +public static class CountryExtension +{ + /// + /// Country code to emoji flag mapping for common countries + /// + private static readonly Dictionary CountryEmojiMap = new(StringComparer.OrdinalIgnoreCase) + { + // Asia + { "CN", "🇨🇳" }, // China + { "HK", "🇭🇰" }, // Hong Kong + { "TW", "🇹🇼" }, // Taiwan + { "JP", "🇯🇵" }, // Japan + { "SG", "🇸🇬" }, // Singapore + { "KR", "🇰🇷" }, // South Korea + { "TH", "🇹🇭" }, // Thailand + { "VN", "🇻🇳" }, // Vietnam + { "ID", "🇮🇩" }, // Indonesia + { "PH", "🇵🇭" }, // Philippines + { "MY", "🇲🇾" }, // Malaysia + { "IN", "🇮🇳" }, // India + { "PK", "🇵🇰" }, // Pakistan + { "BD", "🇧🇩" }, // Bangladesh + { "LK", "🇱🇰" }, // Sri Lanka + { "KH", "🇰🇭" }, // Cambodia + { "LA", "🇱🇦" }, // Laos + { "MM", "🇲🇲" }, // Myanmar + + // Americas + { "US", "🇺🇸" }, // United States + { "CA", "🇨🇦" }, // Canada + { "MX", "🇲🇽" }, // Mexico + { "BR", "🇧🇷" }, // Brazil + { "AR", "🇦🇷" }, // Argentina + { "CL", "🇨🇱" }, // Chile + { "CO", "🇨🇴" }, // Colombia + + // Europe + { "GB", "🇬🇧" }, // United Kingdom + { "DE", "🇩🇪" }, // Germany + { "FR", "🇫🇷" }, // France + { "IT", "🇮🇹" }, // Italy + { "ES", "🇪🇸" }, // Spain + { "RU", "🇷🇺" }, // Russia + { "NL", "🇳🇱" }, // Netherlands + { "CH", "🇨🇭" }, // Switzerland + { "SE", "🇸🇪" }, // Sweden + { "NO", "🇳🇴" }, // Norway + { "DK", "🇩🇰" }, // Denmark + { "FI", "🇫🇮" }, // Finland + { "PL", "🇵🇱" }, // Poland + { "CZ", "🇨🇿" }, // Czech Republic + { "AT", "🇦🇹" }, // Austria + { "GR", "🇬🇷" }, // Greece + { "PT", "🇵🇹" }, // Portugal + { "TR", "🇹🇷" }, // Turkey + { "UA", "🇺🇦" }, // Ukraine + { "RO", "🇷🇴" }, // Romania + + // Middle East & Central Asia + { "AE", "🇦🇪" }, // United Arab Emirates + { "SA", "🇸🇦" }, // Saudi Arabia + { "IL", "🇮🇱" }, // Israel + { "KZ", "🇰🇿" }, // Kazakhstan + + // Oceania + { "AU", "🇦🇺" }, // Australia + { "NZ", "🇳🇿" }, // New Zealand + + // Africa + { "ZA", "🇿🇦" }, // South Africa + { "EG", "🇪🇬" }, // Egypt + }; + + /// + /// Converts country code to flag emoji using predefined mapping + /// Example: "US" -> "🇺🇸", "CN" -> "🇨🇳" + /// + public static string? CountryToEmoji(this string? countryCode) + { + if (countryCode.IsNullOrEmpty()) + { + return null; + } + + return CountryEmojiMap.TryGetValue(countryCode, out var emoji) ? emoji : null; + } +} diff --git a/v2rayN/ServiceLib/Enums/EServerColName.cs b/v2rayN/ServiceLib/Enums/EServerColName.cs index 9f50f4df..8800cdf6 100644 --- a/v2rayN/ServiceLib/Enums/EServerColName.cs +++ b/v2rayN/ServiceLib/Enums/EServerColName.cs @@ -12,6 +12,7 @@ public enum EServerColName SubRemarks, DelayVal, SpeedVal, + IpInfo, TodayDown, TodayUp, diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 8f04f0f9..b06499b4 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -943,6 +943,7 @@ public static class ConfigHandler EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(), EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(), EServerColName.SpeedVal => lstProfile.OrderBy(t => t.Speed).ToList(), + EServerColName.IpInfo => lstProfile.OrderBy(t => t.IpInfo).ToList(), EServerColName.SubRemarks => lstProfile.OrderBy(t => t.Subid).ToList(), EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(), EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(), @@ -963,6 +964,7 @@ public static class ConfigHandler EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(), EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(), EServerColName.SpeedVal => lstProfile.OrderByDescending(t => t.Speed).ToList(), + EServerColName.IpInfo => lstProfile.OrderByDescending(t => t.IpInfo).ToList(), EServerColName.SubRemarks => lstProfile.OrderByDescending(t => t.Subid).ToList(), EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(), EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(), diff --git a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs index 6fd7d453..6e8df9f8 100644 --- a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs @@ -10,7 +10,7 @@ public static class ConnectionHandler public static async Task RunAvailabilityCheck() { var time = await GetRealPingTimeInfo(); - var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None; + var ip = time > 0 ? await GetIPInfo() : Global.None; return string.Format(ResUI.TestMeOutput, time, ip); } @@ -21,7 +21,9 @@ public static class ConnectionHandler private static async Task GetIPInfo() { var webProxy = await GetWebProxy(); - return await GetIPInfo(webProxy); + + var ipInfo = await GetIPInfo(webProxy); + return ipInfo?.ToString() ?? Global.None; } /// @@ -33,11 +35,10 @@ public static class ConnectionHandler try { var webProxy = await GetWebProxy(); - var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl; for (var i = 0; i < 2; i++) { - responseTime = await GetRealPingTime(url, webProxy, 10); + responseTime = await GetRealPingTime(webProxy, 10); if (responseTime > 0) { break; @@ -65,8 +66,9 @@ public static class ConnectionHandler /// /// Measures response time by sending HTTP requests through proxy. /// - public static async Task GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout) + public static async Task GetRealPingTime(IWebProxy? webProxy, int downloadTimeout) { + var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl; var responseTime = -1; try { @@ -98,30 +100,37 @@ public static class ConnectionHandler /// /// Gets IP and country information through specified proxy. /// - public static async Task GetIPInfo(IWebProxy? webProxy) + public static async Task GetIPInfo(IWebProxy? webProxy) { - var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl; - if (url.IsNullOrEmpty()) + try + { + var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl; + if (url.IsNullOrEmpty()) + { + return null; + } + + var downloadHandle = new DownloadService(); + var result = await downloadHandle.TryDownloadString(url, webProxy, ""); + if (result == null) + { + return null; + } + + var ipInfo = JsonUtils.Deserialize(result); + if (ipInfo == null) + { + return null; + } + + var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query; + var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code ?? "unknown"; + + return new IpInfoResult(country, ip); + } + catch { return null; } - - var downloadHandle = new DownloadService(); - var result = await downloadHandle.TryDownloadString(url, webProxy, ""); - if (result == null) - { - return null; - } - - var ipInfo = JsonUtils.Deserialize(result); - if (ipInfo == null) - { - return null; - } - - var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query; - var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code; - - return $"({country ?? "unknown"}) {ip}"; } } diff --git a/v2rayN/ServiceLib/Manager/ProfileExManager.cs b/v2rayN/ServiceLib/Manager/ProfileExManager.cs index 739bd550..cbe90a09 100644 --- a/v2rayN/ServiceLib/Manager/ProfileExManager.cs +++ b/v2rayN/ServiceLib/Manager/ProfileExManager.cs @@ -150,6 +150,14 @@ public class ProfileExManager IndexIdEnqueue(indexId); } + public void SetTestIpInfo(string indexId, string ipInfo) + { + var profileEx = GetProfileExItem(indexId); + + profileEx.IpInfo = ipInfo; + IndexIdEnqueue(indexId); + } + public void SetSort(string indexId, int sort) { var profileEx = GetProfileExItem(indexId); diff --git a/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs b/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs index e1e14363..dddd84d5 100644 --- a/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs +++ b/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs @@ -17,3 +17,12 @@ public class LocationInfo { public string? country_code { get; set; } } + +public readonly record struct IpInfoResult(string Country, string? Ip) +{ + public override string ToString() + { + var emoji = Country.CountryToEmoji(); + return $"{emoji}({Country}) {Ip}"; + } +} diff --git a/v2rayN/ServiceLib/Models/Dto/ProfileItemModel.cs b/v2rayN/ServiceLib/Models/Dto/ProfileItemModel.cs index b75835a4..7c8b96df 100644 --- a/v2rayN/ServiceLib/Models/Dto/ProfileItemModel.cs +++ b/v2rayN/ServiceLib/Models/Dto/ProfileItemModel.cs @@ -26,6 +26,9 @@ public class ProfileItemModel : ReactiveObject [Reactive] public string SpeedVal { get; set; } + [Reactive] + public string IpInfo { get; set; } + [Reactive] public string TodayUp { get; set; } diff --git a/v2rayN/ServiceLib/Models/Dto/SpeedTestResult.cs b/v2rayN/ServiceLib/Models/Dto/SpeedTestResult.cs index bac7bf0f..2e0ab1a5 100644 --- a/v2rayN/ServiceLib/Models/Dto/SpeedTestResult.cs +++ b/v2rayN/ServiceLib/Models/Dto/SpeedTestResult.cs @@ -8,4 +8,6 @@ public class SpeedTestResult public string? Delay { get; set; } public string? Speed { get; set; } + + public string? IpInfo { get; set; } } diff --git a/v2rayN/ServiceLib/Models/Entities/ProfileExItem.cs b/v2rayN/ServiceLib/Models/Entities/ProfileExItem.cs index 7d2cc789..85d1d536 100644 --- a/v2rayN/ServiceLib/Models/Entities/ProfileExItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/ProfileExItem.cs @@ -10,4 +10,5 @@ public class ProfileExItem public decimal Speed { get; set; } public int Sort { get; set; } public string? Message { get; set; } + public string? IpInfo { get; set; } } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 207e7062..84733c1b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -564,6 +564,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 IP Info 的本地化字符串。 + /// + public static string LvTestIpInfo { + get { + return ResourceManager.GetString("LvTestIpInfo", resourceCulture); + } + } + /// /// 查找类似 Speed (MB/s) 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index bd793f88..184337a6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1740,4 +1740,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Not Support + + IP Info + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index 15304814..230bf23f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1737,4 +1737,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Not Support + + IP Info + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index cb114faa..562a36a6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1740,4 +1740,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Not Support + + IP Info + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 26854dd5..d5c0e10a 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1740,4 +1740,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Not Support + + IP Info + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 75c7af32..e93b8c01 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1740,4 +1740,7 @@ Not Support + + IP Info + \ 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 d21444b1..6515df45 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1737,4 +1737,7 @@ 不支持 + + IP 信息 + \ 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 22f6ee71..111b1a95 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1737,4 +1737,7 @@ 不支援 + + IP 資訊 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs index 251522ba..08258160 100644 --- a/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -392,10 +392,23 @@ public class SpeedtestService(Config config, Func updateF private async Task DoRealPing(ServerTestItem it) { var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); - var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); + var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, 10); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); await UpdateFunc(it.IndexId, responseTime.ToString()); + + if (responseTime > 0) + { + var ipInfo = await ConnectionHandler.GetIPInfo(webProxy); + var ipStr = ipInfo?.ToString() ?? Global.None; + ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr); + await UpdateIpInfoFunc(it.IndexId, ipStr); + } + else + { + await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip); + } + return responseTime; } @@ -491,4 +504,9 @@ public class SpeedtestService(Config config, Func updateF ProfileExManager.Instance.SetTestMessage(indexId, speed); } } + + private async Task UpdateIpInfoFunc(string indexId, string ip) + { + await _updateFunc?.Invoke(new() { IndexId = indexId, IpInfo = ip }); + } } diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 609ae0d1..1af0ac14 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -303,6 +303,10 @@ public class ProfilesViewModel : MyReactiveObject { item.SpeedVal = result.Speed ?? string.Empty; } + if (result.IpInfo.IsNotEmpty()) + { + item.IpInfo = result.IpInfo ?? string.Empty; + } await Task.CompletedTask; } @@ -437,6 +441,7 @@ public class ProfilesViewModel : MyReactiveObject Speed = t33?.Speed ?? 0, DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty, SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty, + IpInfo = t33?.IpInfo ?? string.Empty, TodayDown = t22 == null ? "" : Utils.HumanFy(t22.TodayDown), TodayUp = t22 == null ? "" : Utils.HumanFy(t22.TodayUp), TotalDown = t22 == null ? "" : Utils.HumanFy(t22.TotalDown), diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml index a567ae6a..5d19f8f6 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml @@ -277,6 +277,12 @@ Header="{x:Static resx:ResUI.LvTestSpeed}" Tag="SpeedVal" /> + + + + +