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" />
+
+
+
+
+