From 0c796a157bff788694c59a570ec29e63ac240105 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Wed, 20 May 2026 19:16:58 +0800 Subject: [PATCH] =?UTF-8?q?Add=20IP=20info=20&=20flag=20emoji=20to=20test?= =?UTF-8?q?=EF=BC=8Cadd=20ip=20info=20column=20for=20main=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fetch and display tested server IP and country (with emoji) in speed tests. Adds CountryExtension for country->emoji mapping and a new IpInfoResult type; ConnectionHandler now retrieves/parses IP API results and GetRealPingTime signature adjusted. Models and entities (ProfileItemModel, ProfileExItem, SpeedTestResult) gain IpInfo fields; ProfileExManager can store test IP info. UI/UX updated: new IpInfo column in ProfilesView (desktop and Avalonia), ResUI resource strings for "IP Info", and EServerColName ordering supports IpInfo. SpeedtestService now captures IP info and forwards it to the view model via the update function. --- v2rayN/ServiceLib/Common/CountryExtension.cs | 92 +++++++++++++++++++ v2rayN/ServiceLib/Enums/EServerColName.cs | 1 + v2rayN/ServiceLib/Handler/ConfigHandler.cs | 2 + .../ServiceLib/Handler/ConnectionHandler.cs | 61 ++++++------ v2rayN/ServiceLib/Manager/ProfileExManager.cs | 8 ++ v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs | 9 ++ .../ServiceLib/Models/Dto/ProfileItemModel.cs | 3 + .../ServiceLib/Models/Dto/SpeedTestResult.cs | 2 + .../Models/Entities/ProfileExItem.cs | 1 + v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 9 ++ v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.fr.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.hu.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.ru.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 3 + .../ServiceLib/Services/SpeedtestService.cs | 20 +++- .../ViewModels/ProfilesViewModel.cs | 5 + .../v2rayN.Desktop/Views/ProfilesView.axaml | 6 ++ v2rayN/v2rayN/Views/ProfilesView.xaml | 8 ++ 21 files changed, 221 insertions(+), 27 deletions(-) create mode 100644 v2rayN/ServiceLib/Common/CountryExtension.cs 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" /> + + + + +