Compare commits

..

36 commits

Author SHA1 Message Date
DHR60
a079d1a650
Merge a6f2b47952 into a452bbe140 2025-10-04 12:19:29 +00:00
DHR60
a6f2b47952 PreCheck 2025-10-04 20:17:57 +08:00
DHR60
07af03ab81 Remove unnecessary checks 2025-10-04 20:17:57 +08:00
DHR60
8f45d9730b Refactor 2025-10-04 20:17:57 +08:00
2dust
33f5d20022 Update ProfileGroupItem.cs 2025-10-04 20:17:57 +08:00
DHR60
e2df1bc6cb Fix 2025-10-04 20:17:57 +08:00
DHR60
ef09be7a26 Fix 2025-10-04 20:17:57 +08:00
DHR60
142940118e Avoids circular dependency in profile groups
Adds cycle detection to prevent infinite loops when evaluating profile groups.

This ensures that profile group configurations don't result in stack overflow errors when groups reference each other, directly or indirectly.
2025-10-04 20:17:57 +08:00
DHR60
f64f72ba7f Improves Tun2Socks address handling 2025-10-04 20:17:57 +08:00
DHR60
f3adb57e68 Fix 2025-10-04 20:17:57 +08:00
DHR60
ca80e1e831 Avoid self-reference 2025-10-04 20:17:56 +08:00
DHR60
901f8101f0 Add chain selection control to group outbounds 2025-10-04 20:17:56 +08:00
DHR60
587686ffce Refactor 2025-10-04 20:17:56 +08:00
DHR60
aab6d3d136 Add helper function 2025-10-04 20:17:56 +08:00
DHR60
cb814e1dac Adjust chained proxy, actual outbound is at the top
Based on actual network flow instead of data packets
2025-10-04 20:17:56 +08:00
DHR60
8a707cfb90 Refactor 2025-10-04 20:17:56 +08:00
DHR60
4a37b53fe1 Avoid duplicate tags 2025-10-04 20:17:56 +08:00
DHR60
ff6ce3334a Add group in traffic splitting support 2025-10-04 20:17:56 +08:00
DHR60
c00b0b7c43 Add PolicyGroup include other Group support 2025-10-04 20:17:56 +08:00
DHR60
c767e8f085 Add fallback support 2025-10-04 20:17:56 +08:00
DHR60
e7b07f735d Fix 2025-10-04 20:17:56 +08:00
DHR60
3d8559d06d Add Proxy Chain support 2025-10-04 20:17:48 +08:00
DHR60
c98566f270 Adjust UI 2025-10-04 20:08:11 +08:00
DHR60
6f84515f1a Add generate policy group 2025-10-04 20:08:11 +08:00
DHR60
637137303a Add Policy Group support 2025-10-04 20:08:11 +08:00
DHR60
764a2dc301 Rename 2025-10-04 20:08:11 +08:00
DHR60
f4805a399b Exclude specific profile types from selection 2025-10-04 20:08:11 +08:00
DHR60
9202390fa1 Fix right click not working 2025-10-04 20:08:11 +08:00
DHR60
d074d9a72a avalonia 2025-10-04 20:08:11 +08:00
DHR60
dd2bfd9511 VM and wpf 2025-10-04 20:08:11 +08:00
DHR60
55de37e5f3 Multi Profile 2025-10-04 20:08:11 +08:00
2dust
a452bbe140 Fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/8061
2025-10-04 19:54:15 +08:00
DHR60
185c5e4bfb
Fix (#8057) 2025-10-04 16:17:39 +08:00
2dust
bbe64aa970 Remove AutoCompleteBox
https://github.com/2dust/v2rayN/pull/8067
2025-10-04 16:16:32 +08:00
DHR60
513662d89a
Use editable ComboBox instead of AutoCompleteBox (#8067)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
* Update Avalonia

* Use editable ComboBox instead of AutoCompleteBox
2025-10-04 15:18:37 +08:00
2dust
22f0d04f01 Fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/8060
2025-10-03 14:13:03 +08:00
44 changed files with 512 additions and 470 deletions

View file

@ -6,10 +6,10 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.6" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.6" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.7" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.7" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.7" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.7" />
<PackageVersion Include="CliWrap" Version="3.9.0" />
<PackageVersion Include="Downloader" Version="4.0.3" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
@ -19,9 +19,9 @@
<PackageVersion Include="ReactiveUI" Version="20.4.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.10" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.7" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.10" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7" />
<PackageVersion Include="NLog" Version="6.0.4" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />

View file

@ -85,13 +85,19 @@ public class Utils
/// Base64 Encode
/// </summary>
/// <param name="plainText"></param>
/// <param name="removePadding"></param>
/// <returns></returns>
public static string Base64Encode(string plainText)
public static string Base64Encode(string plainText, bool removePadding = false)
{
try
{
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes);
var base64 = Convert.ToBase64String(plainTextBytes);
if (removePadding)
{
base64 = base64.TrimEnd('=');
}
return base64;
}
catch (Exception ex)
{
@ -112,7 +118,7 @@ public class Utils
{
if (plainText.IsNullOrEmpty())
{
return "";
return string.Empty;
}
plainText = plainText.Trim()
@ -947,7 +953,7 @@ public class Utils
if (SetUnixFileMode(fileName))
{
Logging.SaveLog($"Successfully set the file execution permission, {fileName}");
return "";
return string.Empty;
}
if (fileName.Contains(' '))

View file

@ -1087,6 +1087,8 @@ public static class ConfigHandler
profileItem.IndexId = Utils.GetGuid(false);
maxSort = ProfileExManager.Instance.GetMaxSort();
}
var groupType = profileItem.ConfigType == EConfigType.ProxyChain ? EConfigType.ProxyChain.ToString() : profileGroupItem.MultipleLoad.ToString();
profileItem.Address = $"{profileItem.CoreType}-{groupType}";
if (maxSort > 0)
{
ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1);
@ -1194,10 +1196,10 @@ public static class ConfigHandler
var indexId = Utils.GetGuid(false);
var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList());
var remark = string.Empty;
var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId)).Remarks} ";
if (coreType == ECoreType.Xray)
{
remark = multipleLoad switch
remark += multipleLoad switch
{
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback,
@ -1209,7 +1211,7 @@ public static class ConfigHandler
}
else if (coreType == ECoreType.sing_box)
{
remark = multipleLoad switch
remark += multipleLoad switch
{
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback,
@ -1222,7 +1224,6 @@ public static class ConfigHandler
CoreType = coreType,
ConfigType = EConfigType.PolicyGroup,
Remarks = remark,
Address = childProfileIndexId,
};
if (!subId.IsNullOrEmpty())
{
@ -1256,35 +1257,7 @@ public static class ConfigHandler
var tun2SocksAddress = node.Address;
if (node.ConfigType > EConfigType.Group)
{
static async Task<List<string>> GetChildNodeAddressesAsync(string parentIndexId)
{
var childAddresses = new List<string>();
if (!ProfileGroupItemManager.Instance.TryGet(parentIndexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty())
return childAddresses;
var childIds = Utils.String2List(groupItem.ChildItems);
foreach (var childId in childIds)
{
var childNode = await AppManager.Instance.GetProfileItem(childId);
if (childNode == null)
continue;
if (!childNode.IsComplex())
{
childAddresses.Add(childNode.Address);
}
else if (childNode.ConfigType > EConfigType.Group)
{
var subAddresses = await GetChildNodeAddressesAsync(childNode.IndexId);
childAddresses.AddRange(subAddresses);
}
}
return childAddresses;
}
var lstAddresses = await GetChildNodeAddressesAsync(node.IndexId);
var lstAddresses = (await ProfileGroupItemManager.GetAllChildDomainAddresses(node.IndexId)).ToList();
if (lstAddresses.Count > 0)
{
tun2SocksAddress = Utils.List2String(lstAddresses);

View file

@ -155,61 +155,60 @@ public class BaseFmt
protected static int ResolveStdTransport(NameValueCollection query, ref ProfileItem item)
{
item.Flow = query["flow"] ?? "";
item.StreamSecurity = query["security"] ?? "";
item.Sni = query["sni"] ?? "";
item.Alpn = Utils.UrlDecode(query["alpn"] ?? "");
item.Fingerprint = Utils.UrlDecode(query["fp"] ?? "");
item.PublicKey = Utils.UrlDecode(query["pbk"] ?? "");
item.ShortId = Utils.UrlDecode(query["sid"] ?? "");
item.SpiderX = Utils.UrlDecode(query["spx"] ?? "");
item.Mldsa65Verify = Utils.UrlDecode(query["pqv"] ?? "");
item.AllowInsecure = (query["allowInsecure"] ?? "") == "1" ? "true" : "";
item.Flow = GetQueryValue(query, "flow");
item.StreamSecurity = GetQueryValue(query, "security");
item.Sni = GetQueryValue(query, "sni");
item.Alpn = GetQueryDecoded(query, "alpn");
item.Fingerprint = GetQueryDecoded(query, "fp");
item.PublicKey = GetQueryDecoded(query, "pbk");
item.ShortId = GetQueryDecoded(query, "sid");
item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.AllowInsecure = new[] { "allowInsecure", "allow_insecure", "insecure" }.Any(k => (query[k] ?? "") == "1") ? "true" : "";
item.Network = query["type"] ?? nameof(ETransport.tcp);
item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp));
switch (item.Network)
{
case nameof(ETransport.tcp):
item.HeaderType = query["headerType"] ?? Global.None;
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
item.RequestHost = GetQueryDecoded(query, "host");
break;
case nameof(ETransport.kcp):
item.HeaderType = query["headerType"] ?? Global.None;
item.Path = Utils.UrlDecode(query["seed"] ?? "");
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
item.Path = GetQueryDecoded(query, "seed");
break;
case nameof(ETransport.ws):
case nameof(ETransport.httpupgrade):
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.Path = Utils.UrlDecode(query["path"] ?? "/");
item.RequestHost = GetQueryDecoded(query, "host");
item.Path = GetQueryDecoded(query, "path", "/");
break;
case nameof(ETransport.xhttp):
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.Path = Utils.UrlDecode(query["path"] ?? "/");
item.HeaderType = Utils.UrlDecode(query["mode"] ?? "");
item.Extra = Utils.UrlDecode(query["extra"] ?? "");
item.RequestHost = GetQueryDecoded(query, "host");
item.Path = GetQueryDecoded(query, "path", "/");
item.HeaderType = GetQueryDecoded(query, "mode");
item.Extra = GetQueryDecoded(query, "extra");
break;
case nameof(ETransport.http):
case nameof(ETransport.h2):
item.Network = nameof(ETransport.h2);
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.Path = Utils.UrlDecode(query["path"] ?? "/");
item.RequestHost = GetQueryDecoded(query, "host");
item.Path = GetQueryDecoded(query, "path", "/");
break;
case nameof(ETransport.quic):
item.HeaderType = query["headerType"] ?? Global.None;
item.RequestHost = query["quicSecurity"] ?? Global.None;
item.Path = Utils.UrlDecode(query["key"] ?? "");
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
item.RequestHost = GetQueryValue(query, "quicSecurity", Global.None);
item.Path = GetQueryDecoded(query, "key");
break;
case nameof(ETransport.grpc):
item.RequestHost = Utils.UrlDecode(query["authority"] ?? "");
item.Path = Utils.UrlDecode(query["serviceName"] ?? "");
item.HeaderType = Utils.UrlDecode(query["mode"] ?? Global.GrpcGunMode);
item.RequestHost = GetQueryDecoded(query, "authority");
item.Path = GetQueryDecoded(query, "serviceName");
item.HeaderType = GetQueryDecoded(query, "mode", Global.GrpcGunMode);
break;
default:
@ -239,4 +238,14 @@ public class BaseFmt
var url = $"{Utils.UrlEncode(userInfo)}@{GetIpv6(address)}:{port}";
return $"{Global.ProtocolShares[eConfigType]}{url}{query}{remark}";
}
protected static string GetQueryValue(NameValueCollection query, string key, string defaultValue = "")
{
return query[key] ?? defaultValue;
}
protected static string GetQueryDecoded(NameValueCollection query, string key, string defaultValue = "")
{
return Utils.UrlDecode(GetQueryValue(query, key, defaultValue));
}
}

View file

@ -27,7 +27,7 @@ public class FmtHandler
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return "";
return string.Empty;
}
}

View file

@ -21,10 +21,10 @@ public class Hysteria2Fmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item);
item.Path = Utils.UrlDecode(query["obfs-password"] ?? "");
item.AllowInsecure = (query["insecure"] ?? "") == "1" ? "true" : "false";
item.Path = GetQueryDecoded(query, "obfs-password");
item.AllowInsecure = GetQueryValue(query, "insecure") == "1" ? "true" : "false";
item.Ports = Utils.UrlDecode(query["mport"] ?? "");
item.Ports = GetQueryDecoded(query, "mport");
return item;
}

View file

@ -42,7 +42,7 @@ public class ShadowsocksFmt : BaseFmt
// item.port);
//url = Utile.Base64Encode(url);
//new Sip002
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}");
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
return ToUri(EConfigType.Shadowsocks, item.Address, item.Port, pw, null, remark);
}

View file

@ -33,7 +33,7 @@ public class SocksFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks);
}
//new
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}");
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
return ToUri(EConfigType.SOCKS, item.Address, item.Port, pw, null, remark);
}

View file

@ -30,7 +30,7 @@ public class TuicFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item);
item.HeaderType = query["congestion_control"] ?? "";
item.HeaderType = GetQueryValue(query, "congestion_control");
return item;
}

View file

@ -24,8 +24,8 @@ public class VLESSFmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query);
item.Security = query["encryption"] ?? Global.None;
item.StreamSecurity = query["security"] ?? "";
item.Security = GetQueryValue(query, "encryption", Global.None);
item.StreamSecurity = GetQueryValue(query, "security");
_ = ResolveStdTransport(query, ref item);
return item;

View file

@ -24,10 +24,10 @@ public class WireguardFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
item.PublicKey = Utils.UrlDecode(query["publickey"] ?? "");
item.Path = Utils.UrlDecode(query["reserved"] ?? "");
item.RequestHost = Utils.UrlDecode(query["address"] ?? "");
item.ShortId = Utils.UrlDecode(query["mtu"] ?? "");
item.PublicKey = GetQueryDecoded(query, "publickey");
item.Path = GetQueryDecoded(query, "reserved");
item.RequestHost = GetQueryDecoded(query, "address");
item.ShortId = GetQueryDecoded(query, "mtu");
return item;
}

View file

@ -8,7 +8,6 @@ public sealed class AppManager
private Config _config;
private int? _statePort;
private int? _statePort2;
private WindowsJob? _processJob;
public static AppManager Instance => _instance.Value;
public Config Config => _config;
@ -100,7 +99,6 @@ public sealed class AppManager
await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo();
await ProfileGroupItemManager.Instance.SaveTo();
await StatisticsManager.Instance.SaveTo();
await CoreManager.Instance.CoreStop();
StatisticsManager.Instance.Close();
@ -138,21 +136,6 @@ public sealed class AppManager
return localPort + (int)protocol;
}
public void AddProcess(nint processHandle)
{
if (Utils.IsWindows())
{
_processJob ??= new();
try
{
_processJob?.AddProcess(processHandle);
}
catch
{
}
}
}
#endregion Config
#region SqliteHelper

View file

@ -8,6 +8,7 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value;
private Config _config;
private WindowsJob? _processJob;
private ProcessService? _processService;
private ProcessService? _processPreService;
private bool _linuxSudo = false;
@ -264,14 +265,28 @@ public class CoreManager
await procService.StartAsync();
await Task.Delay(100);
AppManager.Instance.AddProcess(procService.Handle);
if (procService is null or { HasExited: true })
{
throw new Exception(ResUI.FailedToRunCore);
}
AddProcessJob(procService.Handle);
return procService;
}
private void AddProcessJob(nint processHandle)
{
if (Utils.IsWindows())
{
_processJob ??= new();
try
{
_processJob?.AddProcess(processHandle);
}
catch { }
}
}
#endregion Process
}

View file

@ -164,4 +164,113 @@ public class ProfileGroupItemManager
Logging.SaveLog(_tag, ex);
}
}
#region Helper
public static bool HasCycle(string? indexId)
{
return HasCycle(indexId, new HashSet<string>(), new HashSet<string>());
}
public static bool HasCycle(string? indexId, HashSet<string> visited, HashSet<string> stack)
{
if (indexId.IsNullOrEmpty())
return false;
if (stack.Contains(indexId))
return true;
if (visited.Contains(indexId))
return false;
visited.Add(indexId);
stack.Add(indexId);
Instance.TryGet(indexId, out var groupItem);
if (groupItem == null || groupItem.ChildItems.IsNullOrEmpty())
{
return false;
}
var childIds = Utils.String2List(groupItem.ChildItems)
.Where(p => !string.IsNullOrEmpty(p))
.ToList();
foreach (var child in childIds)
{
if (HasCycle(child, visited, stack))
{
return true;
}
}
stack.Remove(indexId);
return false;
}
public static async Task<(List<ProfileItem> Items, ProfileGroupItem? Group)> GetChildProfileItems(string? indexId)
{
Instance.TryGet(indexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return (new List<ProfileItem>(), profileGroupItem);
}
var items = await GetChildProfileItems(profileGroupItem);
return (items, profileGroupItem);
}
public static async Task<List<ProfileItem>> GetChildProfileItems(ProfileGroupItem? group)
{
if (group == null || group.ChildItems.IsNullOrEmpty())
{
return new();
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(group.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p =>
p != null &&
p.IsValid() &&
p.ConfigType != EConfigType.Custom
)
.ToList();
return childProfiles;
}
public static async Task<HashSet<string>> GetAllChildDomainAddresses(string parentIndexId)
{
// include grand children
var childAddresses = new HashSet<string>();
if (!Instance.TryGet(parentIndexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty())
return childAddresses;
var childIds = Utils.String2List(groupItem.ChildItems);
foreach (var childId in childIds)
{
var childNode = await AppManager.Instance.GetProfileItem(childId);
if (childNode == null)
continue;
if (!childNode.IsComplex())
{
childAddresses.Add(childNode.Address);
}
else if (childNode.ConfigType > EConfigType.Group)
{
var subAddresses = await GetAllChildDomainAddresses(childNode.IndexId);
foreach (var addr in subAddresses)
{
childAddresses.Add(addr);
}
}
}
return childAddresses;
}
#endregion Helper
}

View file

@ -35,7 +35,6 @@ public class TaskManager
await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo();
await ProfileGroupItemManager.Instance.SaveTo();
}
//Execute once 1 hour

View file

@ -1,4 +1,5 @@
using SQLite;
namespace ServiceLib.Models;
[Serializable]

View file

@ -109,42 +109,6 @@ public class ProfileItem : ReactiveObject
return true;
}
public async Task<bool> HasCycle(HashSet<string> visited, HashSet<string> stack)
{
if (ConfigType < EConfigType.Group)
return false;
if (stack.Contains(IndexId))
return true;
if (visited.Contains(IndexId))
return false;
visited.Add(IndexId);
stack.Add(IndexId);
if (ProfileGroupItemManager.Instance.TryGet(IndexId, out var group)
&& !group.ChildItems.IsNullOrEmpty())
{
var childProfiles = (await Task.WhenAll(
Utils.String2List(group.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p => p != null)
.ToList();
foreach (var child in childProfiles)
{
if (await child.HasCycle(visited, stack))
return true;
}
}
stack.Remove(IndexId);
return false;
}
#endregion function
[PrimaryKey]

View file

@ -3166,16 +3166,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 The sing-box DoH resolution server can be overwritten 的本地化字符串。
/// </summary>
public static string TbSBDoHOverride {
get {
return ResourceManager.GetString("TbSBDoHOverride", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback DNS Resolution, Require IP 的本地化字符串。
/// 查找类似 Resolve DNS server domains, requires IP 的本地化字符串。
/// </summary>
public static string TbSBFallbackDNSResolve {
get {

View file

@ -1429,7 +1429,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Require IP</value>
<value>Resolve DNS server domains, requires IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
@ -1443,9 +1443,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1429,7 +1429,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Require IP</value>
<value>Resolve DNS server domains, requires IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
@ -1443,9 +1443,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1429,7 +1429,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Require IP</value>
<value>Resolve DNS server domains, requires IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
@ -1443,9 +1443,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1429,7 +1429,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Require IP</value>
<value>Resolve DNS server domains, requires IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>Стратегия резолвинга Freedom (Xray)</value>
@ -1443,9 +1443,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Добавить стандартные записи hosts (DNS)</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>Сервер DoH-резолвера sing-box можно переопределить</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1426,7 +1426,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>回退 DNS 解析,需指定为 IP</value>
<value>解析 DNS 服务器域名,需指定为 IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray freedom 解析策略</value>
@ -1440,9 +1440,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>添加常用 DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>开启后可覆盖 sing-box DoH 解析服务器</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1426,7 +1426,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Require IP</value>
<value>Resolve DNS server domains, requires IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
@ -1440,9 +1440,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1,3 +1,5 @@
using DynamicData;
namespace ServiceLib.Services;
/// <summary>
@ -124,7 +126,7 @@ public class ActionPrecheckService(Config config)
return errors;
}
var hasCycle = await item.HasCycle(new HashSet<string>(), new HashSet<string>());
var hasCycle = ProfileGroupItemManager.HasCycle(item.IndexId);
if (hasCycle)
{
errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks));

View file

@ -363,8 +363,6 @@ public partial class CoreConfigSingboxService(Config config)
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
if (groupRet != 0)
@ -373,6 +371,8 @@ public partial class CoreConfigSingboxService(Config config)
return ret;
}
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
@ -416,12 +416,10 @@ public partial class CoreConfigSingboxService(Config config)
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
singboxConfig.outbounds.RemoveAt(0);
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
singboxConfig.outbounds.RemoveAt(0);
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
if (groupRet != 0)
@ -430,6 +428,8 @@ public partial class CoreConfigSingboxService(Config config)
return ret;
}
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);

View file

@ -212,33 +212,13 @@ public partial class CoreConfigSingboxService
{
return -1;
}
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return -1;
}
var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (hasCycle)
{
return -1;
}
// remove custom nodes
// remove group nodes for proxy chain
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p =>
p != null
&& p.IsValid()
&& p.ConfigType != EConfigType.Custom
&& (node.ConfigType == EConfigType.PolicyGroup || p.ConfigType < EConfigType.Group)
)
.ToList();
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
return -1;
@ -514,16 +494,7 @@ public partial class CoreConfigSingboxService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
continue;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
@ -640,12 +611,12 @@ public partial class CoreConfigSingboxService
}
// Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(singboxConfig.outbounds);
singboxConfig.outbounds = resultOutbounds;
singboxConfig.endpoints ??= new List<Endpoints4Sbox>();
resultEndpoints.AddRange(singboxConfig.endpoints);
singboxConfig.endpoints = resultEndpoints;
var serverList = new List<BaseServer4Sbox>();
serverList = serverList.Concat(prevOutbounds)
.Concat(resultOutbounds)
.Concat(resultEndpoints)
.ToList();
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
}
catch (Exception ex)
{
@ -694,6 +665,30 @@ public partial class CoreConfigSingboxService
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
if (node == null)
continue;
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{i + 1}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
if (ret == 0)
{
proxyTags.Add(childBaseTagName);
}
continue;
}
var server = await GenServer(node);
if (server is null)
{
@ -737,12 +732,11 @@ public partial class CoreConfigSingboxService
resultOutbounds.Insert(0, outUrltest);
resultOutbounds.Insert(0, outSelector);
}
singboxConfig.outbounds ??= new();
resultOutbounds.AddRange(singboxConfig.outbounds);
singboxConfig.outbounds = resultOutbounds;
singboxConfig.endpoints ??= new();
resultEndpoints.AddRange(singboxConfig.endpoints);
singboxConfig.endpoints = resultEndpoints;
var serverList = new List<BaseServer4Sbox>();
serverList = serverList.Concat(resultOutbounds)
.Concat(resultEndpoints)
.ToList();
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
return await Task.FromResult(0);
}
@ -785,14 +779,40 @@ public partial class CoreConfigSingboxService
resultOutbounds.Add(outbound);
}
}
singboxConfig.outbounds ??= new();
resultOutbounds.AddRange(singboxConfig.outbounds);
singboxConfig.outbounds = resultOutbounds;
singboxConfig.endpoints ??= new();
resultEndpoints.AddRange(singboxConfig.endpoints);
singboxConfig.endpoints = resultEndpoints;
var serverList = new List<BaseServer4Sbox>();
serverList = serverList.Concat(resultOutbounds)
.Concat(resultEndpoints)
.ToList();
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
return await Task.FromResult(0);
}
private async Task<int> AddRangeOutbounds(List<BaseServer4Sbox> servers, SingboxConfig singboxConfig, bool prepend = true)
{
try
{
if (servers is null || servers.Count <= 0)
{
return 0;
}
var outbounds = servers.Where(s => s is Outbound4Sbox).Cast<Outbound4Sbox>().ToList();
var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast<Endpoints4Sbox>().ToList();
singboxConfig.endpoints ??= new();
if (prepend)
{
singboxConfig.outbounds.InsertRange(0, outbounds);
singboxConfig.endpoints.InsertRange(0, endpoints);
}
else
{
singboxConfig.outbounds.AddRange(outbounds);
singboxConfig.endpoints.AddRange(endpoints);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
}

View file

@ -376,7 +376,7 @@ public partial class CoreConfigSingboxService
return Global.ProxyTag;
}
var tag = Global.ProxyTag + node.IndexId.ToString();
var tag = $"{node.IndexId}-{Global.ProxyTag}";
if (singboxConfig.outbounds.Any(o => o.tag == tag)
|| (singboxConfig.endpoints != null && singboxConfig.endpoints.Any(e => e.tag == tag)))
{
@ -385,11 +385,10 @@ public partial class CoreConfigSingboxService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}";
var ret = await GenGroupOutbound(node, singboxConfig, childBaseTagName);
var ret = await GenGroupOutbound(node, singboxConfig, tag);
if (ret == 0)
{
return childBaseTagName;
return tag;
}
return Global.ProxyTag;
}

View file

@ -114,9 +114,6 @@ public partial class CoreConfigV2rayService(Config config)
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
if (groupRet != 0)
@ -125,11 +122,15 @@ public partial class CoreConfigV2rayService(Config config)
return ret;
}
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}";
//add rule
var rules = v2rayConfig.routing.rules;
if (rules?.Count > 0)
if (rules?.Count > 0 && ((v2rayConfig.routing.balancers?.Count ?? 0) > 0))
{
var balancerTagSet = v2rayConfig.routing.balancers
.Select(b => b.tag)
@ -176,7 +177,7 @@ public partial class CoreConfigV2rayService(Config config)
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true);
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
return ret;
}
catch (Exception ex)
@ -215,13 +216,10 @@ public partial class CoreConfigV2rayService(Config config)
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
v2rayConfig.outbounds.RemoveAt(0);
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
v2rayConfig.outbounds.RemoveAt(0);
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
if (groupRet != 0)
@ -230,9 +228,13 @@ public partial class CoreConfigV2rayService(Config config)
return ret;
}
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true);
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
return ret;
}
catch (Exception ex)

View file

@ -4,32 +4,80 @@ public partial class CoreConfigV2rayService
{
private async Task<int> GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
{
if (multipleLoad == EMultipleLoad.LeastPing)
// Collect all existing subject selectors from both observatories
var subjectSelectors = new List<string>();
subjectSelectors.AddRange(v2rayConfig.burstObservatory?.subjectSelector ?? []);
subjectSelectors.AddRange(v2rayConfig.observatory?.subjectSelector ?? []);
// Case 1: exact match already exists -> nothing to do
if (subjectSelectors.Any(baseTagName.StartsWith))
return await Task.FromResult(0);
// Case 2: prefix match exists -> reuse it and move to the first position
var matched = subjectSelectors.FirstOrDefault(s => s.StartsWith(baseTagName));
if (matched is not null)
{
var observatory = new Observatory4Ray
baseTagName = matched;
if (v2rayConfig.burstObservatory?.subjectSelector?.Contains(baseTagName) == true)
{
subjectSelector = [baseTagName],
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
probeInterval = "3m",
enableConcurrency = true,
};
v2rayConfig.observatory = observatory;
v2rayConfig.burstObservatory.subjectSelector.Remove(baseTagName);
v2rayConfig.burstObservatory.subjectSelector.Insert(0, baseTagName);
}
if (v2rayConfig.observatory?.subjectSelector?.Contains(baseTagName) == true)
{
v2rayConfig.observatory.subjectSelector.Remove(baseTagName);
v2rayConfig.observatory.subjectSelector.Insert(0, baseTagName);
}
return await Task.FromResult(0);
}
else if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback)
// Case 3: need to create or insert based on multipleLoad type
if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback)
{
var burstObservatory = new BurstObservatory4Ray
if (v2rayConfig.burstObservatory is null)
{
subjectSelector = [baseTagName],
pingConfig = new()
// Create new burst observatory with default ping config
v2rayConfig.burstObservatory = new BurstObservatory4Ray
{
destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
interval = "5m",
timeout = "30s",
sampling = 2,
}
};
v2rayConfig.burstObservatory = burstObservatory;
subjectSelector = [baseTagName],
pingConfig = new()
{
destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
interval = "5m",
timeout = "30s",
sampling = 2,
}
};
}
else
{
v2rayConfig.burstObservatory.subjectSelector ??= new();
v2rayConfig.burstObservatory.subjectSelector.Add(baseTagName);
}
}
else if (multipleLoad is EMultipleLoad.LeastPing)
{
if (v2rayConfig.observatory is null)
{
// Create new observatory with default probe config
v2rayConfig.observatory = new Observatory4Ray
{
subjectSelector = [baseTagName],
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
probeInterval = "3m",
enableConcurrency = true,
};
}
else
{
v2rayConfig.observatory.subjectSelector ??= new();
v2rayConfig.observatory.subjectSelector.Add(baseTagName);
}
}
return await Task.FromResult(0);
}

View file

@ -4,7 +4,7 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<string> ApplyFullConfigTemplate(V2rayConfig v2rayConfig, bool handleBalancerAndRules = false)
private async Task<string> ApplyFullConfigTemplate(V2rayConfig v2rayConfig)
{
var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty())
@ -19,7 +19,7 @@ public partial class CoreConfigV2rayService
}
// Handle balancer and rules modifications (for multiple load scenarios)
if (handleBalancerAndRules && v2rayConfig.routing?.balancers?.Count > 0)
if (v2rayConfig.routing?.balancers?.Count > 0)
{
var balancer = v2rayConfig.routing.balancers.First();
@ -60,6 +60,34 @@ public partial class CoreConfigV2rayService
}
}
if (v2rayConfig.observatory != null)
{
if (fullConfigTemplateNode["observatory"] == null)
{
fullConfigTemplateNode["observatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.observatory));
}
else
{
var subjectSelector = v2rayConfig.observatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["observatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
}
}
if (v2rayConfig.burstObservatory != null)
{
if (fullConfigTemplateNode["burstObservatory"] == null)
{
fullConfigTemplateNode["burstObservatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.burstObservatory));
}
else
{
var subjectSelector = v2rayConfig.burstObservatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["burstObservatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
}
}
// Handle outbounds - append instead of override
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
foreach (var outbound in v2rayConfig.outbounds)

View file

@ -488,33 +488,13 @@ public partial class CoreConfigV2rayService
{
return -1;
}
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return -1;
}
var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (hasCycle)
{
return -1;
}
// remove custom nodes
// remove group nodes for proxy chain
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p =>
p != null &&
p.IsValid() &&
p.ConfigType != EConfigType.Custom &&
(node.ConfigType == EConfigType.PolicyGroup || p.ConfigType < EConfigType.Group)
)
.ToList();
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
return -1;
@ -539,9 +519,11 @@ public partial class CoreConfigV2rayService
}
//add balancers
await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
if (node.ConfigType == EConfigType.PolicyGroup)
{
await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
}
}
catch (Exception ex)
{
@ -649,16 +631,7 @@ public partial class CoreConfigV2rayService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
continue;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
@ -726,9 +699,17 @@ public partial class CoreConfigV2rayService
}
// Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(prevOutbounds);
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
}
catch (Exception ex)
{
@ -797,6 +778,26 @@ public partial class CoreConfigV2rayService
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
if (node == null)
continue;
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{i + 1}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
continue;
}
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
@ -811,13 +812,19 @@ public partial class CoreConfigV2rayService
outbound.tag = baseTagName + (i + 1).ToString();
resultOutbounds.Add(outbound);
}
v2rayConfig.outbounds ??= new();
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
return await Task.FromResult(0);
}
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, V2rayConfig v2RayConfig, string baseTagName = Global.ProxyTag)
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag)
{
// Based on actual network flow instead of data packets
var nodesReverse = nodes.AsEnumerable().Reverse().ToList();
@ -858,9 +865,15 @@ public partial class CoreConfigV2rayService
resultOutbounds.Add(outbound);
}
v2RayConfig.outbounds ??= new();
resultOutbounds.AddRange(v2RayConfig.outbounds);
v2RayConfig.outbounds = resultOutbounds;
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
return await Task.FromResult(0);
}

View file

@ -133,7 +133,7 @@ public partial class CoreConfigV2rayService
return Global.ProxyTag;
}
var tag = Global.ProxyTag + node.IndexId.ToString();
var tag = $"{node.IndexId}-{Global.ProxyTag}";
if (v2rayConfig.outbounds.Any(p => p.tag == tag))
{
return tag;
@ -141,11 +141,10 @@ public partial class CoreConfigV2rayService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}";
var ret = await GenGroupOutbound(node, v2rayConfig, childBaseTagName);
var ret = await GenGroupOutbound(node, v2rayConfig, tag);
if (ret == 0)
{
return childBaseTagName;
return tag;
}
return Global.ProxyTag;
}

View file

@ -195,12 +195,12 @@ public class AddGroupServerViewModel : MyReactiveObject
var childIndexIds = new List<string>();
foreach (var item in ChildItemsObs)
{
if (!item.IndexId.IsNullOrEmpty())
if (item.IndexId.IsNullOrEmpty())
{
childIndexIds.Add(item.IndexId);
continue;
}
childIndexIds.Add(item.IndexId);
}
SelectedSource.Address = Utils.List2String(childIndexIds);
var profileGroup = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource.IndexId);
profileGroup.ChildItems = Utils.List2String(childIndexIds);
profileGroup.MultipleLoad = PolicyGroupType switch

View file

@ -20,7 +20,6 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="Assets/GlobalResources.axaml" />
<ResourceInclude Source="Controls/AutoCompleteBox.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

View file

@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
@ -18,7 +19,7 @@ internal class AvaUtils
return null;
}
return await clipboard.GetTextAsync();
return await clipboard.TryGetTextAsync();
}
catch
{
@ -33,9 +34,7 @@ internal class AvaUtils
var clipboard = TopLevel.GetTopLevel(visual)?.Clipboard;
if (clipboard == null)
return;
var dataObject = new DataObject();
dataObject.Set(DataFormats.Text, strData);
await clipboard.SetDataObjectAsync(dataObject);
await clipboard.SetTextAsync(strData);
}
catch
{

View file

@ -1,48 +0,0 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:v2rayN.Desktop.Controls">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type controls:AutoCompleteBox}" TargetType="controls:AutoCompleteBox">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="MinHeight" Value="{DynamicResource AutoCompleteBoxDefaultHeight}" />
<Setter Property="MaxDropDownHeight" Value="{DynamicResource AutoCompleteMaxDropdownHeight}" />
<Setter Property="Template">
<ControlTemplate TargetType="AutoCompleteBox">
<Panel>
<TextBox
Name="PART_TextBox"
MinHeight="{TemplateBinding MinHeight}"
VerticalAlignment="Stretch"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
InnerLeftContent="{TemplateBinding InnerLeftContent}"
InnerRightContent="{TemplateBinding InnerRightContent}"
Watermark="{TemplateBinding Watermark}" />
<Popup
Name="PART_Popup"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
IsLightDismissEnabled="True"
PlacementTarget="{TemplateBinding}">
<Border
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
Margin="{DynamicResource AutoCompleteBoxPopupMargin}"
Padding="{DynamicResource AutoCompleteBoxPopupPadding}"
HorizontalAlignment="Stretch"
Background="{DynamicResource AutoCompleteBoxPopupBackground}"
BorderBrush="{DynamicResource AutoCompleteBoxPopupBorderBrush}"
BorderThickness="{DynamicResource AutoCompleteBoxPopupBorderThickness}"
BoxShadow="{DynamicResource AutoCompleteBoxPopupBoxShadow}"
CornerRadius="{DynamicResource AutoCompleteBoxPopupCornerRadius}">
<ListBox
Name="PART_SelectingItemsControl"
Foreground="{TemplateBinding Foreground}"
ItemTemplate="{TemplateBinding ItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Border>
</Popup>
</Panel>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

View file

@ -1,40 +0,0 @@
using Avalonia.Input;
using Avalonia.Interactivity;
namespace v2rayN.Desktop.Controls;
public class AutoCompleteBox : Avalonia.Controls.AutoCompleteBox
{
static AutoCompleteBox()
{
MinimumPrefixLengthProperty.OverrideDefaultValue<AutoCompleteBox>(0);
}
public AutoCompleteBox()
{
AddHandler(PointerPressedEvent, OnBoxPointerPressed, RoutingStrategies.Tunnel);
}
private void OnBoxPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (Equals(sender, this) && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
SetCurrentValue(IsDropDownOpenProperty, true);
}
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (IsDropDownOpen)
{
return;
}
SetCurrentValue(IsDropDownOpenProperty, true);
}
public void Clear()
{
SetCurrentValue(SelectedItemProperty, null);
}
}

View file

@ -2,7 +2,6 @@
x:Class="v2rayN.Desktop.Views.DNSSettingWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrls="clr-namespace:v2rayN.Desktop.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@ -56,13 +55,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbDomesticDNS}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbDirectDNS"
Grid.Row="1"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
Text="{Binding DirectDNS, Mode=TwoWay}" />
IsEditable="True" />
<TextBlock
Grid.Row="2"
@ -70,13 +69,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRemoteDNS}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbRemoteDNS"
Grid.Row="2"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
Text="{Binding RemoteDNS, Mode=TwoWay}" />
IsEditable="True" />
<TextBlock
Grid.Row="3"
@ -84,13 +83,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBOutboundsResolverDNS}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbSBResolverDNS"
Grid.Row="3"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
Text="{Binding SingboxOutboundsResolveDNS, Mode=TwoWay}" />
IsEditable="True" />
<TextBlock
Grid.Row="3"
Grid.Column="2"
@ -105,13 +104,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBBootstrapDNS}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbSBFinalResolverDNS"
Grid.Row="4"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
Text="{Binding SingboxFinalResolveDNS, Mode=TwoWay}" />
IsEditable="True" />
<TextBlock
Grid.Row="4"
Grid.Column="2"
@ -174,13 +173,6 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBDoHOverride}"
TextWrapping="Wrap" />
</Grid>
</ScrollViewer>
</TabItem>
@ -260,13 +252,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbDirectExpectedIPs"
Grid.Row="4"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
Text="{Binding DirectExpectedIPs, Mode=TwoWay}" />
IsEditable="True" />
<TextBlock
Grid.Row="4"
Grid.Column="2"
@ -363,11 +355,11 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbdomainDNSAddressCompatible"
Width="150"
Margin="{StaticResource Margin4}"
Text="{Binding DomainDNSAddressCompatible, Mode=TwoWay}" />
IsEditable="True" />
</StackPanel>
</WrapPanel>
@ -435,11 +427,11 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbdomainDNSAddress2Compatible"
Width="150"
Margin="{StaticResource Margin4}"
Text="{Binding DomainDNSAddress2Compatible, Mode=TwoWay}" />
IsEditable="True" />
</StackPanel>
</WrapPanel>

View file

@ -40,15 +40,15 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.Bind(ViewModel, vm => vm.AddCommonHosts, v => v.togAddCommonHosts.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.SingboxOutboundsResolveDNS, v => v.cmbSBResolverDNS.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.SingboxFinalResolveDNS, v => v.cmbSBFinalResolverDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxOutboundsResolveDNS, v => v.cmbSBResolverDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxFinalResolveDNS, v => v.cmbSBFinalResolverDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RayStrategy4Freedom, v => v.cmbRayFreedomDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
@ -57,11 +57,11 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.Bind(ViewModel, vm => vm.UseSystemHostsCompatible, v => v.togUseSystemHostsCompatible.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainStrategy4FreedomCompatible, v => v.cmbdomainStrategy4FreedomCompatible.SelectedItem).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NormalDNSCompatible, v => v.txtnormalDNSCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainStrategy4Freedom2Compatible, v => v.cmbdomainStrategy4OutCompatible.SelectedItem).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NormalDNS2Compatible, v => v.txtnormalDNS2Compatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunDNS2Compatible, v => v.txttunDNS2Compatible.Text).DisposeWith(disposables);

View file

@ -2,7 +2,6 @@
x:Class="v2rayN.Desktop.Views.OptionSettingWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrls="clr-namespace:v2rayN.Desktop.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@ -502,12 +501,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsCurrentFontFamily}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbcurrentFontFamily"
Grid.Row="15"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}" />
Margin="{StaticResource Margin4}"
IsEditable="True" />
<TextBlock
Grid.Row="15"
Grid.Column="2"
@ -548,12 +548,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSpeedTestUrl}" />
<ctrls:AutoCompleteBox
<ComboBox
Name="cmbSpeedTestUrl"
Grid.Row="18"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}" />
Margin="{StaticResource Margin4}"
IsEditable="True" />
<TextBlock
Grid.Row="19"
@ -561,12 +562,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSpeedPingTestUrl}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbSpeedPingTestUrl"
Grid.Row="19"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}" />
Margin="{StaticResource Margin4}"
IsEditable="True" />
<TextBlock
Grid.Row="20"
@ -587,12 +589,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSubConvert}" />
<ctrls:AutoCompleteBox
<ComboBox
x:Name="cmbSubConvertUrl"
Grid.Row="21"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}" />
Margin="{StaticResource Margin4}"
IsEditable="True" />
<TextBlock
Grid.Row="22"
@ -618,7 +621,8 @@
Grid.Row="23"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}" />
Margin="{StaticResource Margin4}"
IsEditable="True" />
<TextBlock
Grid.Row="23"
Grid.Column="2"
@ -638,7 +642,8 @@
Grid.Row="24"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}" />
Margin="{StaticResource Margin4}"
IsEditable="True" />
<TextBlock
Grid.Row="24"
Grid.Column="2"
@ -658,7 +663,8 @@
Grid.Row="25"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}" />
Margin="{StaticResource Margin4}"
IsEditable="True" />
<TextBlock
Grid.Row="25"
Grid.Column="2"

View file

@ -2,7 +2,6 @@
x:Class="v2rayN.Desktop.Views.RoutingRuleDetailsWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrls="clr-namespace:v2rayN.Desktop.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@ -47,28 +46,26 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="outboundTag" />
<ctrls:AutoCompleteBox
<ComboBox
Name="cmbOutboundTag"
Grid.Row="1"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
Text="{Binding SelectedSource.OutboundTag, Mode=TwoWay}" />
IsEditable="True" />
<StackPanel
Grid.Row="1"
Grid.Column="2"
Margin="{StaticResource Margin4}"
Orientation="Horizontal"
HorizontalAlignment="Left"
VerticalAlignment="Center">
VerticalAlignment="Center"
Orientation="Horizontal">
<Button
x:Name="btnSelectProfile"
Margin="0,0,8,0"
Content="{x:Static resx:ResUI.TbSelectProfile}"
Click="BtnSelectProfile_Click" />
<TextBlock
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
Click="BtnSelectProfile_Click"
Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock VerticalAlignment="Center" Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
</StackPanel>
<TextBlock

View file

@ -43,6 +43,7 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables);

View file

@ -210,13 +210,6 @@
Grid.Column="1"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="3"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSBDoHOverride}" />
</Grid>
</ScrollViewer>
</TabItem>