Compare commits

..

39 commits

Author SHA1 Message Date
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
2dust
d7c5161431 Optimize and improve
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
2025-10-02 19:55:49 +08:00
2dust
12cc09d0c9 Bug 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
2025-10-01 20:17:26 +08:00
2dust
5b12c36da5 Optimize and improve, encapsulate ProcessService 2025-10-01 19:49:28 +08:00
DHR60
e970372a9f
Fix some minor UI bugs (#8053) 2025-10-01 16:47:22 +08:00
53 changed files with 814 additions and 759 deletions

View file

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

View file

@ -67,116 +67,4 @@ public static class ProcUtils
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
} }
public static async Task ProcessKill(int pid)
{
try
{
await ProcessKill(Process.GetProcessById(pid), false);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
public static async Task ProcessKill(Process? proc, bool review)
{
if (proc is null)
{
return;
}
GetProcessKeyInfo(proc, review, out var procId, out var fileName, out var processName);
try
{
if (Utils.IsNonWindows())
{
proc?.Kill(true);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
try
{
proc?.Kill();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
try
{
proc?.Close();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
try
{
proc?.Dispose();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
await Task.Delay(300);
await ProcessKillByKeyInfo(review, procId, fileName, processName);
}
private static void GetProcessKeyInfo(Process? proc, bool review, out int? procId, out string? fileName, out string? processName)
{
procId = null;
fileName = null;
processName = null;
if (!review)
{
return;
}
try
{
procId = proc?.Id;
fileName = proc?.MainModule?.FileName;
processName = proc?.ProcessName;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
private static async Task ProcessKillByKeyInfo(bool review, int? procId, string? fileName, string? processName)
{
if (review && procId != null && fileName != null)
{
try
{
var lstProc = Process.GetProcessesByName(processName);
foreach (var proc2 in lstProc)
{
if (proc2.Id == procId)
{
Logging.SaveLog($"{_tag}, KillProcess not completing the job, procId");
await ProcessKill(proc2, false);
}
if (proc2.MainModule != null && proc2.MainModule?.FileName == fileName)
{
Logging.SaveLog($"{_tag}, KillProcess not completing the job, fileName");
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
}
} }

View file

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

View file

@ -7,11 +7,11 @@ namespace ServiceLib.Common;
* http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net * http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net
*/ */
public sealed class Job : IDisposable public sealed class WindowsJob : IDisposable
{ {
private IntPtr handle = IntPtr.Zero; private IntPtr handle = IntPtr.Zero;
public Job() public WindowsJob()
{ {
handle = CreateJobObject(IntPtr.Zero, null); handle = CreateJobObject(IntPtr.Zero, null);
var extendedInfoPtr = IntPtr.Zero; var extendedInfoPtr = IntPtr.Zero;
@ -94,7 +94,7 @@ namespace ServiceLib.Common;
} }
} }
~Job() ~WindowsJob()
{ {
Dispose(false); Dispose(false);
} }

View file

@ -1087,6 +1087,8 @@ public static class ConfigHandler
profileItem.IndexId = Utils.GetGuid(false); profileItem.IndexId = Utils.GetGuid(false);
maxSort = ProfileExManager.Instance.GetMaxSort(); maxSort = ProfileExManager.Instance.GetMaxSort();
} }
var groupType = profileItem.ConfigType == EConfigType.ProxyChain ? EConfigType.ProxyChain.ToString() : profileGroupItem.MultipleLoad.ToString();
profileItem.Address = $"{profileItem.CoreType}-{groupType}";
if (maxSort > 0) if (maxSort > 0)
{ {
ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1); ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1);
@ -1194,10 +1196,10 @@ public static class ConfigHandler
var indexId = Utils.GetGuid(false); var indexId = Utils.GetGuid(false);
var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList()); 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) if (coreType == ECoreType.Xray)
{ {
remark = multipleLoad switch remark += multipleLoad switch
{ {
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing, EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback, EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback,
@ -1209,7 +1211,7 @@ public static class ConfigHandler
} }
else if (coreType == ECoreType.sing_box) else if (coreType == ECoreType.sing_box)
{ {
remark = multipleLoad switch remark += multipleLoad switch
{ {
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing, EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback, EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback,
@ -1222,7 +1224,6 @@ public static class ConfigHandler
CoreType = coreType, CoreType = coreType,
ConfigType = EConfigType.PolicyGroup, ConfigType = EConfigType.PolicyGroup,
Remarks = remark, Remarks = remark,
Address = childProfileIndexId,
}; };
if (!subId.IsNullOrEmpty()) if (!subId.IsNullOrEmpty())
{ {
@ -1256,35 +1257,7 @@ public static class ConfigHandler
var tun2SocksAddress = node.Address; var tun2SocksAddress = node.Address;
if (node.ConfigType > EConfigType.Group) if (node.ConfigType > EConfigType.Group)
{ {
static async Task<List<string>> GetChildNodeAddressesAsync(string parentIndexId) var lstAddresses = (await ProfileGroupItemManager.GetAllChildDomainAddresses(node.IndexId)).ToList();
{
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);
if (lstAddresses.Count > 0) if (lstAddresses.Count > 0)
{ {
tun2SocksAddress = Utils.List2String(lstAddresses); tun2SocksAddress = Utils.List2String(lstAddresses);

View file

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

View file

@ -42,7 +42,7 @@ public class ShadowsocksFmt : BaseFmt
// item.port); // item.port);
//url = Utile.Base64Encode(url); //url = Utile.Base64Encode(url);
//new Sip002 //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); 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); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
//new //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); 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); var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item); ResolveStdTransport(query, ref item);
item.HeaderType = query["congestion_control"] ?? ""; item.HeaderType = GetQueryValue(query, "congestion_control");
return item; return item;
} }

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Text; using System.Text;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
@ -31,7 +30,7 @@ public class CoreAdminManager
await _updateFunc?.Invoke(notify, msg); await _updateFunc?.Invoke(notify, msg);
} }
public async Task<Process?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath) public async Task<ProcessService?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine("#!/bin/bash"); sb.AppendLine("#!/bin/bash");
@ -39,50 +38,25 @@ public class CoreAdminManager
sb.AppendLine($"sudo -S {cmdLine}"); sb.AppendLine($"sudo -S {cmdLine}");
var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true); var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
Process proc = new() var procService = new ProcessService(
{ fileName: shFilePath,
StartInfo = new() arguments: "",
{ workingDirectory: Utils.GetBinConfigPath(),
FileName = shFilePath, displayLog: true,
Arguments = "", redirectInput: true,
WorkingDirectory = Utils.GetBinConfigPath(), environmentVars: null,
UseShellExecute = false, updateFunc: _updateFunc
RedirectStandardInput = true, );
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
}
};
void dataHandler(object sender, DataReceivedEventArgs e) await procService.StartAsync(AppManager.Instance.LinuxSudoPwd);
{
if (e.Data.IsNotEmpty())
{
_ = UpdateFunc(false, e.Data + Environment.NewLine);
}
}
proc.OutputDataReceived += dataHandler; if (procService is null or { HasExited: true })
proc.ErrorDataReceived += dataHandler;
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
await Task.Delay(10);
await proc.StandardInput.WriteLineAsync(AppManager.Instance.LinuxSudoPwd);
await Task.Delay(100);
if (proc is null or { HasExited: true })
{ {
throw new Exception(ResUI.FailedToRunCore); throw new Exception(ResUI.FailedToRunCore);
} }
_linuxSudoPid = procService.Id;
_linuxSudoPid = proc.Id; return procService;
return proc;
} }
public async Task KillProcessAsLinuxSudo() public async Task KillProcessAsLinuxSudo()

View file

@ -1,6 +1,3 @@
using System.Diagnostics;
using System.Text;
namespace ServiceLib.Manager; namespace ServiceLib.Manager;
/// <summary> /// <summary>
@ -11,8 +8,9 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new()); private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value; public static CoreManager Instance => _instance.Value;
private Config _config; private Config _config;
private Process? _process; private WindowsJob? _processJob;
private Process? _processPre; private ProcessService? _processService;
private ProcessService? _processPreService;
private bool _linuxSudo = false; private bool _linuxSudo = false;
private Func<bool, string, Task>? _updateFunc; private Func<bool, string, Task>? _updateFunc;
private const string _tag = "CoreHandler"; private const string _tag = "CoreHandler";
@ -89,13 +87,13 @@ public class CoreManager
await CoreStart(node); await CoreStart(node);
await CoreStartPreService(node); await CoreStartPreService(node);
if (_process != null) if (_processService != null)
{ {
await UpdateFunc(true, $"{node.GetSummary()}"); await UpdateFunc(true, $"{node.GetSummary()}");
} }
} }
public async Task<int> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds) public async Task<ProcessService?> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds)
{ {
var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) ? ECoreType.sing_box : ECoreType.Xray; var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) ? ECoreType.sing_box : ECoreType.Xray;
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
@ -104,28 +102,22 @@ public class CoreManager
await UpdateFunc(false, result.Msg); await UpdateFunc(false, result.Msg);
if (result.Success != true) if (result.Success != true)
{ {
return -1; return null;
} }
await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
await UpdateFunc(false, configPath); await UpdateFunc(false, configPath);
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var proc = await RunProcess(coreInfo, fileName, true, false); return await RunProcess(coreInfo, fileName, true, false);
if (proc is null)
{
return -1;
}
return proc.Id;
} }
public async Task<int> LoadCoreConfigSpeedtest(ServerTestItem testItem) public async Task<ProcessService?> LoadCoreConfigSpeedtest(ServerTestItem testItem)
{ {
var node = await AppManager.Instance.GetProfileItem(testItem.IndexId); var node = await AppManager.Instance.GetProfileItem(testItem.IndexId);
if (node is null) if (node is null)
{ {
return -1; return null;
} }
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
@ -133,18 +125,12 @@ public class CoreManager
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, node, testItem, configPath); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, node, testItem, configPath);
if (result.Success != true) if (result.Success != true)
{ {
return -1; return null;
} }
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var proc = await RunProcess(coreInfo, fileName, true, false); return await RunProcess(coreInfo, fileName, true, false);
if (proc is null)
{
return -1;
}
return proc.Id;
} }
public async Task CoreStop() public async Task CoreStop()
@ -157,16 +143,18 @@ public class CoreManager
_linuxSudo = false; _linuxSudo = false;
} }
if (_process != null) if (_processService != null)
{ {
await ProcUtils.ProcessKill(_process, Utils.IsWindows()); await _processService.StopAsync();
_process = null; _processService.Dispose();
_processService = null;
} }
if (_processPre != null) if (_processPreService != null)
{ {
await ProcUtils.ProcessKill(_processPre, Utils.IsWindows()); await _processPreService.StopAsync();
_processPre = null; _processPreService.Dispose();
_processPreService = null;
} }
} }
catch (Exception ex) catch (Exception ex)
@ -188,12 +176,12 @@ public class CoreManager
{ {
return; return;
} }
_process = proc; _processService = proc;
} }
private async Task CoreStartPreService(ProfileItem node) private async Task CoreStartPreService(ProfileItem node)
{ {
if (_process != null && !_process.HasExited) if (_processService != null && !_processService.HasExited)
{ {
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType); var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType);
@ -210,7 +198,7 @@ public class CoreManager
{ {
return; return;
} }
_processPre = proc; _processPreService = proc;
} }
} }
} }
@ -225,7 +213,7 @@ public class CoreManager
#region Process #region Process
private async Task<Process?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo) private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
{ {
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg); var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
@ -256,55 +244,48 @@ public class CoreManager
} }
} }
private async Task<Process?> RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog) private async Task<ProcessService?> RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog)
{ {
Process proc = new() var environmentVars = new Dictionary<string, string>();
{
StartInfo = new()
{
FileName = fileName,
Arguments = string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath),
WorkingDirectory = Utils.GetBinConfigPath(),
UseShellExecute = false,
RedirectStandardOutput = displayLog,
RedirectStandardError = displayLog,
CreateNoWindow = true,
StandardOutputEncoding = displayLog ? Encoding.UTF8 : null,
StandardErrorEncoding = displayLog ? Encoding.UTF8 : null,
}
};
foreach (var kv in coreInfo.Environment) foreach (var kv in coreInfo.Environment)
{ {
proc.StartInfo.Environment[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath); environmentVars[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath);
} }
if (displayLog) var procService = new ProcessService(
{ fileName: fileName,
void dataHandler(object sender, DataReceivedEventArgs e) arguments: string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath),
{ workingDirectory: Utils.GetBinConfigPath(),
if (e.Data.IsNotEmpty()) displayLog: displayLog,
{ redirectInput: false,
_ = UpdateFunc(false, e.Data + Environment.NewLine); environmentVars: environmentVars,
} updateFunc: _updateFunc
} );
proc.OutputDataReceived += dataHandler;
proc.ErrorDataReceived += dataHandler;
}
proc.Start();
if (displayLog) await procService.StartAsync();
{
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
}
await Task.Delay(100); await Task.Delay(100);
AppManager.Instance.AddProcess(proc.Handle);
if (proc is null or { HasExited: true }) if (procService is null or { HasExited: true })
{ {
throw new Exception(ResUI.FailedToRunCore); throw new Exception(ResUI.FailedToRunCore);
} }
return proc; AddProcessJob(procService.Handle);
return procService;
}
private void AddProcessJob(nint processHandle)
{
if (Utils.IsWindows())
{
_processJob ??= new();
try
{
_processJob?.AddProcess(processHandle);
}
catch { }
}
} }
#endregion Process #endregion Process

View file

@ -164,4 +164,113 @@ public class ProfileGroupItemManager
Logging.SaveLog(_tag, ex); 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 ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo(); await ProfileExManager.Instance.SaveTo();
await ProfileGroupItemManager.Instance.SaveTo();
} }
//Execute once 1 hour //Execute once 1 hour

View file

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

View file

@ -109,42 +109,6 @@ public class ProfileItem : ReactiveObject
return true; 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 #endregion function
[PrimaryKey] [PrimaryKey]

View file

@ -3147,6 +3147,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Bootstrap DNS (sing-box) 的本地化字符串。
/// </summary>
public static string TbSBBootstrapDNS {
get {
return ResourceManager.GetString("TbSBBootstrapDNS", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 sing-box Direct Resolution Strategy 的本地化字符串。 /// 查找类似 sing-box Direct Resolution Strategy 的本地化字符串。
/// </summary> /// </summary>
@ -3157,25 +3166,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 The sing-box DoH resolution server can be overwritten 的本地化字符串。 /// 查找类似 Resolve DNS server domains, requires IP 的本地化字符串。
/// </summary>
public static string TbSBDoHOverride {
get {
return ResourceManager.GetString("TbSBDoHOverride", resourceCulture);
}
}
/// <summary>
/// 查找类似 sing-box DoH Resolver Server 的本地化字符串。
/// </summary>
public static string TbSBDoHResolverServer {
get {
return ResourceManager.GetString("TbSBDoHResolverServer", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback DNS Resolution, Suggest IP 的本地化字符串。
/// </summary> /// </summary>
public static string TbSBFallbackDNSResolve { public static string TbSBFallbackDNSResolve {
get { get {

View file

@ -1425,11 +1425,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value> <value>Resolve Outbound Domains</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH Resolver Server</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value> <value>Resolve DNS server domains, requires IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value> <value>xray Freedom Resolution Strategy</value>
@ -1443,9 +1443,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve"> <data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value> <value>Add Common DNS Hosts</value>
</data> </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"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
</data> </data>

View file

@ -1425,11 +1425,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value> <value>Resolve Outbound Domains</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH Resolver Server</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value> <value>Resolve DNS server domains, requires IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value> <value>xray Freedom Resolution Strategy</value>
@ -1443,9 +1443,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve"> <data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value> <value>Add Common DNS Hosts</value>
</data> </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"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
</data> </data>

View file

@ -1425,11 +1425,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value> <value>Resolve Outbound Domains</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH Resolver Server</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value> <value>Resolve DNS server domains, requires IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value> <value>xray Freedom Resolution Strategy</value>
@ -1443,9 +1443,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve"> <data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value> <value>Add Common DNS Hosts</value>
</data> </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"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
</data> </data>

View file

@ -1425,11 +1425,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Разрешать домены для исходящих соединений</value> <value>Разрешать домены для исходящих соединений</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>Сервер DoH-резолвера (sing-box)</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Резервное DNS-разрешение (рекомендуется указывать IP)</value> <value>Resolve DNS server domains, requires IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>Стратегия резолвинга Freedom (Xray)</value> <value>Стратегия резолвинга Freedom (Xray)</value>
@ -1443,9 +1443,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve"> <data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Добавить стандартные записи hosts (DNS)</value> <value>Добавить стандартные записи hosts (DNS)</value>
</data> </data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>Сервер DoH-резолвера sing-box можно переопределить</value>
</data>
<data name="TbFakeIP" xml:space="preserve"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
</data> </data>

View file

@ -1422,11 +1422,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>解析出站域名</value> <value>解析出站域名</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH 解析服务器</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>兜底解析其他 DNS 域名,建议设为 ip</value> <value>解析 DNS 服务器域名,需指定为 IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray freedom 解析策略</value> <value>xray freedom 解析策略</value>
@ -1440,9 +1440,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve"> <data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>添加常用 DNS Hosts</value> <value>添加常用 DNS Hosts</value>
</data> </data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>开启后可覆盖 sing-box DoH 解析服务器</value>
</data>
<data name="TbFakeIP" xml:space="preserve"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
</data> </data>

View file

@ -1422,11 +1422,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value> <value>Resolve Outbound Domains</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH Resolver Server</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value> <value>Resolve DNS server domains, requires IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value> <value>xray Freedom Resolution Strategy</value>
@ -1440,9 +1440,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve"> <data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value> <value>Add Common DNS Hosts</value>
</data> </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"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
</data> </data>

View file

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

View file

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

View file

@ -212,33 +212,13 @@ public partial class CoreConfigSingboxService
{ {
return -1; return -1;
} }
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return -1;
}
var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle) if (hasCycle)
{ {
return -1; return -1;
} }
// remove custom nodes var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
// 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();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
return -1; return -1;
@ -514,16 +494,7 @@ public partial class CoreConfigSingboxService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{ {
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
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();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
continue; continue;
@ -640,12 +611,12 @@ public partial class CoreConfigSingboxService
} }
// Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds // Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds
resultOutbounds.AddRange(prevOutbounds); var serverList = new List<BaseServer4Sbox>();
resultOutbounds.AddRange(singboxConfig.outbounds); serverList = serverList.Concat(prevOutbounds)
singboxConfig.outbounds = resultOutbounds; .Concat(resultOutbounds)
singboxConfig.endpoints ??= new List<Endpoints4Sbox>(); .Concat(resultEndpoints)
resultEndpoints.AddRange(singboxConfig.endpoints); .ToList();
singboxConfig.endpoints = resultEndpoints; await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -694,6 +665,30 @@ public partial class CoreConfigSingboxService
for (var i = 0; i < nodes.Count; i++) for (var i = 0; i < nodes.Count; i++)
{ {
var node = nodes[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); var server = await GenServer(node);
if (server is null) if (server is null)
{ {
@ -737,12 +732,11 @@ public partial class CoreConfigSingboxService
resultOutbounds.Insert(0, outUrltest); resultOutbounds.Insert(0, outUrltest);
resultOutbounds.Insert(0, outSelector); resultOutbounds.Insert(0, outSelector);
} }
singboxConfig.outbounds ??= new(); var serverList = new List<BaseServer4Sbox>();
resultOutbounds.AddRange(singboxConfig.outbounds); serverList = serverList.Concat(resultOutbounds)
singboxConfig.outbounds = resultOutbounds; .Concat(resultEndpoints)
singboxConfig.endpoints ??= new(); .ToList();
resultEndpoints.AddRange(singboxConfig.endpoints); await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
singboxConfig.endpoints = resultEndpoints;
return await Task.FromResult(0); return await Task.FromResult(0);
} }
@ -785,14 +779,40 @@ public partial class CoreConfigSingboxService
resultOutbounds.Add(outbound); resultOutbounds.Add(outbound);
} }
} }
singboxConfig.outbounds ??= new(); var serverList = new List<BaseServer4Sbox>();
resultOutbounds.AddRange(singboxConfig.outbounds); serverList = serverList.Concat(resultOutbounds)
singboxConfig.outbounds = resultOutbounds; .Concat(resultEndpoints)
.ToList();
singboxConfig.endpoints ??= new(); await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
resultEndpoints.AddRange(singboxConfig.endpoints); return await Task.FromResult(0);
singboxConfig.endpoints = resultEndpoints; }
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); return await Task.FromResult(0);
} }
} }

View file

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

View file

@ -114,9 +114,6 @@ public partial class CoreConfigV2rayService(Config config)
await GenLog(v2rayConfig); await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig); await GenInbounds(v2rayConfig);
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig); var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
if (groupRet != 0) if (groupRet != 0)
@ -125,11 +122,15 @@ public partial class CoreConfigV2rayService(Config config)
return ret; return ret;
} }
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}"; var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}";
//add rule //add rule
var rules = v2rayConfig.routing.rules; var rules = v2rayConfig.routing.rules;
if (rules?.Count > 0) if (rules?.Count > 0 && ((v2rayConfig.routing.balancers?.Count ?? 0) > 0))
{ {
var balancerTagSet = v2rayConfig.routing.balancers var balancerTagSet = v2rayConfig.routing.balancers
.Select(b => b.tag) .Select(b => b.tag)
@ -176,7 +177,7 @@ public partial class CoreConfigV2rayService(Config config)
ret.Success = true; ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true); ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@ -215,13 +216,10 @@ public partial class CoreConfigV2rayService(Config config)
ret.Msg = ResUI.FailedGenDefaultConfiguration; ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret; return ret;
} }
v2rayConfig.outbounds.RemoveAt(0);
await GenLog(v2rayConfig); await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig); await GenInbounds(v2rayConfig);
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
v2rayConfig.outbounds.RemoveAt(0);
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig); var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
if (groupRet != 0) if (groupRet != 0)
@ -230,9 +228,13 @@ public partial class CoreConfigV2rayService(Config config)
return ret; return ret;
} }
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
ret.Success = true; ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true); ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
return ret; return ret;
} }
catch (Exception ex) 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) 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], v2rayConfig.burstObservatory.subjectSelector.Remove(baseTagName);
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, v2rayConfig.burstObservatory.subjectSelector.Insert(0, baseTagName);
probeInterval = "3m", }
enableConcurrency = true,
}; if (v2rayConfig.observatory?.subjectSelector?.Contains(baseTagName) == true)
v2rayConfig.observatory = observatory; {
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], // Create new burst observatory with default ping config
pingConfig = new() v2rayConfig.burstObservatory = new BurstObservatory4Ray
{ {
destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, subjectSelector = [baseTagName],
interval = "5m", pingConfig = new()
timeout = "30s", {
sampling = 2, destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
} interval = "5m",
}; timeout = "30s",
v2rayConfig.burstObservatory = burstObservatory; 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); return await Task.FromResult(0);
} }

View file

@ -4,7 +4,7 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService 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); var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty()) 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) // 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(); 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 // Handle outbounds - append instead of override
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray(); var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
foreach (var outbound in v2rayConfig.outbounds) foreach (var outbound in v2rayConfig.outbounds)

View file

@ -488,33 +488,13 @@ public partial class CoreConfigV2rayService
{ {
return -1; return -1;
} }
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return -1;
}
var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle) if (hasCycle)
{ {
return -1; return -1;
} }
// remove custom nodes var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
// 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();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
return -1; return -1;
@ -539,9 +519,11 @@ public partial class CoreConfigV2rayService
} }
//add balancers //add balancers
await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); if (node.ConfigType == EConfigType.PolicyGroup)
await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); {
await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -649,16 +631,7 @@ public partial class CoreConfigV2rayService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{ {
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
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();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
continue; continue;
@ -726,9 +699,17 @@ public partial class CoreConfigV2rayService
} }
// Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds // Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
resultOutbounds.AddRange(prevOutbounds); if (baseTagName == Global.ProxyTag)
resultOutbounds.AddRange(v2rayConfig.outbounds); {
v2rayConfig.outbounds = resultOutbounds; resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(prevOutbounds);
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -797,6 +778,26 @@ public partial class CoreConfigV2rayService
for (var i = 0; i < nodes.Count; i++) for (var i = 0; i < nodes.Count; i++)
{ {
var node = nodes[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); var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (txtOutbound.IsNullOrEmpty()) if (txtOutbound.IsNullOrEmpty())
{ {
@ -811,13 +812,19 @@ public partial class CoreConfigV2rayService
outbound.tag = baseTagName + (i + 1).ToString(); outbound.tag = baseTagName + (i + 1).ToString();
resultOutbounds.Add(outbound); resultOutbounds.Add(outbound);
} }
v2rayConfig.outbounds ??= new(); if (baseTagName == Global.ProxyTag)
resultOutbounds.AddRange(v2rayConfig.outbounds); {
v2rayConfig.outbounds = resultOutbounds; resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
return await Task.FromResult(0); 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 // Based on actual network flow instead of data packets
var nodesReverse = nodes.AsEnumerable().Reverse().ToList(); var nodesReverse = nodes.AsEnumerable().Reverse().ToList();
@ -858,9 +865,15 @@ public partial class CoreConfigV2rayService
resultOutbounds.Add(outbound); resultOutbounds.Add(outbound);
} }
v2RayConfig.outbounds ??= new(); if (baseTagName == Global.ProxyTag)
resultOutbounds.AddRange(v2RayConfig.outbounds); {
v2RayConfig.outbounds = resultOutbounds; resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
return await Task.FromResult(0); return await Task.FromResult(0);
} }

View file

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

View file

@ -0,0 +1,183 @@
using System.Diagnostics;
using System.Text;
namespace ServiceLib.Services;
public class ProcessService : IDisposable
{
private readonly Process _process;
private readonly Func<bool, string, Task>? _updateFunc;
private bool _isDisposed;
public int Id => _process.Id;
public IntPtr Handle => _process.Handle;
public bool HasExited => _process.HasExited;
public ProcessService(
string fileName,
string arguments,
string workingDirectory,
bool displayLog,
bool redirectInput,
Dictionary<string, string>? environmentVars,
Func<bool, string, Task>? updateFunc)
{
_updateFunc = updateFunc;
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardInput = redirectInput,
RedirectStandardOutput = displayLog,
RedirectStandardError = displayLog,
CreateNoWindow = true,
StandardOutputEncoding = displayLog ? Encoding.UTF8 : null,
StandardErrorEncoding = displayLog ? Encoding.UTF8 : null,
},
EnableRaisingEvents = true
};
if (environmentVars != null)
{
foreach (var kv in environmentVars)
{
_process.StartInfo.Environment[kv.Key] = kv.Value;
}
}
if (displayLog)
{
RegisterEventHandlers();
}
}
public async Task StartAsync(string pwd = null)
{
_process.Start();
if (_process.StartInfo.RedirectStandardOutput)
{
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
}
if (_process.StartInfo.RedirectStandardInput)
{
await Task.Delay(10);
await _process.StandardInput.WriteLineAsync(pwd);
}
}
public async Task StopAsync()
{
if (_process.HasExited)
{
return;
}
try
{
if (_process.StartInfo.RedirectStandardOutput)
{
try
{
_process.CancelOutputRead();
}
catch { }
try
{
_process.CancelErrorRead();
}
catch { }
}
try
{
if (Utils.IsNonWindows())
{
_process.Kill(true);
}
}
catch { }
try
{
_process.Kill();
}
catch { }
await Task.Delay(100);
}
catch (Exception ex)
{
await _updateFunc?.Invoke(true, ex.Message);
}
}
private void RegisterEventHandlers()
{
void dataHandler(object sender, DataReceivedEventArgs e)
{
if (e.Data.IsNotEmpty())
{
_ = _updateFunc?.Invoke(false, e.Data + Environment.NewLine);
}
}
_process.OutputDataReceived += dataHandler;
_process.ErrorDataReceived += dataHandler;
_process.Exited += (s, e) =>
{
try
{
_process.OutputDataReceived -= dataHandler;
_process.ErrorDataReceived -= dataHandler;
}
catch
{
}
};
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
try
{
if (!_process.HasExited)
{
try
{
_process.CancelOutputRead();
}
catch { }
try
{
_process.CancelErrorRead();
}
catch { }
_process.Kill();
}
_process.Dispose();
}
catch (Exception ex)
{
_updateFunc?.Invoke(true, ex.Message);
}
_isDisposed = true;
GC.SuppressFinalize(this);
}
}

View file

@ -182,11 +182,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds, string exitLoopKey) private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
{ {
var pid = -1; ProcessService processService = null;
try try
{ {
pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds); processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds);
if (pid < 0) if (processService is null)
{ {
return false; return false;
} }
@ -216,10 +216,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
finally finally
{ {
if (pid > 0) await processService?.StopAsync();
{
await ProcUtils.ProcessKill(pid);
}
} }
return true; return true;
} }
@ -244,11 +241,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
tasks.Add(Task.Run(async () => tasks.Add(Task.Run(async () =>
{ {
var pid = -1; ProcessService processService = null;
try try
{ {
pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(it); processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
if (pid < 0) if (processService is null)
{ {
await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
} }
@ -275,10 +272,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
finally finally
{ {
if (pid > 0) await processService?.StopAsync();
{
await ProcUtils.ProcessKill(pid);
}
concurrencySemaphore.Release(); concurrencySemaphore.Release();
} }
})); }));

View file

@ -195,12 +195,12 @@ public class AddGroupServerViewModel : MyReactiveObject
var childIndexIds = new List<string>(); var childIndexIds = new List<string>();
foreach (var item in ChildItemsObs) 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); var profileGroup = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource.IndexId);
profileGroup.ChildItems = Utils.List2String(childIndexIds); profileGroup.ChildItems = Utils.List2String(childIndexIds);
profileGroup.MultipleLoad = PolicyGroupType switch profileGroup.MultipleLoad = PolicyGroupType switch

View file

@ -1,4 +1,5 @@
using System.Reactive; using System.Reactive;
using System.Reactive.Linq;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
@ -32,6 +33,8 @@ public class DNSSettingViewModel : MyReactiveObject
[Reactive] public bool RayCustomDNSEnableCompatible { get; set; } [Reactive] public bool RayCustomDNSEnableCompatible { get; set; }
[Reactive] public bool SBCustomDNSEnableCompatible { get; set; } [Reactive] public bool SBCustomDNSEnableCompatible { get; set; }
[ObservableAsProperty] public bool IsSimpleDNSEnabled { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; } public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; } public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; }
@ -55,6 +58,10 @@ public class DNSSettingViewModel : MyReactiveObject
await Task.CompletedTask; await Task.CompletedTask;
}); });
this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible)
.Select(x => !(x.Item1 && x.Item2))
.ToPropertyEx(this, x => x.IsSimpleDNSEnabled);
_ = Init(); _ = Init();
} }

View file

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

View file

@ -1,6 +1,7 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
@ -18,7 +19,7 @@ internal class AvaUtils
return null; return null;
} }
return await clipboard.GetTextAsync(); return await clipboard.TryGetTextAsync();
} }
catch catch
{ {
@ -33,9 +34,7 @@ internal class AvaUtils
var clipboard = TopLevel.GetTopLevel(visual)?.Clipboard; var clipboard = TopLevel.GetTopLevel(visual)?.Clipboard;
if (clipboard == null) if (clipboard == null)
return; return;
var dataObject = new DataObject(); await clipboard.SetTextAsync(strData);
dataObject.Set(DataFormats.Text, strData);
await clipboard.SetDataObjectAsync(dataObject);
} }
catch 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" x:Class="v2rayN.Desktop.Views.DNSSettingWindow"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 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:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@ -37,6 +36,7 @@
<TabItem Header="{x:Static resx:ResUI.ThBasicDNSSettings}"> <TabItem Header="{x:Static resx:ResUI.ThBasicDNSSettings}">
<ScrollViewer VerticalScrollBarVisibility="Visible"> <ScrollViewer VerticalScrollBarVisibility="Visible">
<Grid <Grid
x:Name="gridBasicDNSSettings"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
ColumnDefinitions="Auto,Auto,*" ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
@ -55,13 +55,13 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbDomesticDNS}" /> Text="{x:Static resx:ResUI.TbDomesticDNS}" />
<ctrls:AutoCompleteBox <ComboBox
x:Name="cmbDirectDNS" x:Name="cmbDirectDNS"
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Width="300" Width="300"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding DirectDNS, Mode=TwoWay}" /> IsEditable="True" />
<TextBlock <TextBlock
Grid.Row="2" Grid.Row="2"
@ -69,13 +69,13 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRemoteDNS}" /> Text="{x:Static resx:ResUI.TbRemoteDNS}" />
<ctrls:AutoCompleteBox <ComboBox
x:Name="cmbRemoteDNS" x:Name="cmbRemoteDNS"
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
Width="300" Width="300"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding RemoteDNS, Mode=TwoWay}" /> IsEditable="True" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
@ -83,13 +83,13 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBOutboundsResolverDNS}" /> Text="{x:Static resx:ResUI.TbSBOutboundsResolverDNS}" />
<ctrls:AutoCompleteBox <ComboBox
x:Name="cmbSBResolverDNS" x:Name="cmbSBResolverDNS"
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
Width="300" Width="300"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding SingboxOutboundsResolveDNS, Mode=TwoWay}" /> IsEditable="True" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="2" Grid.Column="2"
@ -103,14 +103,14 @@
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBDoHResolverServer}" /> Text="{x:Static resx:ResUI.TbSBBootstrapDNS}" />
<ctrls:AutoCompleteBox <ComboBox
x:Name="cmbSBFinalResolverDNS" x:Name="cmbSBFinalResolverDNS"
Grid.Row="4" Grid.Row="4"
Grid.Column="1" Grid.Column="1"
Width="300" Width="300"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding SingboxFinalResolveDNS, Mode=TwoWay}" /> IsEditable="True" />
<TextBlock <TextBlock
Grid.Row="4" Grid.Row="4"
Grid.Column="2" Grid.Column="2"
@ -173,13 +173,6 @@
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBDoHOverride}"
TextWrapping="Wrap" />
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
@ -187,6 +180,7 @@
<TabItem Header="{x:Static resx:ResUI.ThAdvancedDNSSettings}"> <TabItem Header="{x:Static resx:ResUI.ThAdvancedDNSSettings}">
<ScrollViewer VerticalScrollBarVisibility="Visible"> <ScrollViewer VerticalScrollBarVisibility="Visible">
<Grid <Grid
x:Name="gridAdvancedDNSSettings"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
ColumnDefinitions="Auto,Auto,*" ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*">
@ -258,13 +252,13 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" /> Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" />
<ctrls:AutoCompleteBox <ComboBox
x:Name="cmbDirectExpectedIPs" x:Name="cmbDirectExpectedIPs"
Grid.Row="4" Grid.Row="4"
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding DirectExpectedIPs, Mode=TwoWay}" /> IsEditable="True" />
<TextBlock <TextBlock
Grid.Row="4" Grid.Row="4"
Grid.Column="2" Grid.Column="2"
@ -361,11 +355,11 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" /> Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" />
<ctrls:AutoCompleteBox <ComboBox
x:Name="cmbdomainDNSAddressCompatible" x:Name="cmbdomainDNSAddressCompatible"
Width="150" Width="150"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding DomainDNSAddressCompatible, Mode=TwoWay}" /> IsEditable="True" />
</StackPanel> </StackPanel>
</WrapPanel> </WrapPanel>
@ -433,11 +427,11 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" /> Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" />
<ctrls:AutoCompleteBox <ComboBox
x:Name="cmbdomainDNSAddress2Compatible" x:Name="cmbdomainDNSAddress2Compatible"
Width="150" Width="150"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding DomainDNSAddress2Compatible, Mode=TwoWay}" /> IsEditable="True" />
</StackPanel> </StackPanel>
</WrapPanel> </WrapPanel>

View file

@ -1,4 +1,5 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ReactiveUI; using ReactiveUI;
@ -39,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.AddCommonHosts, v => v.togAddCommonHosts.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.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.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.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.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.SingboxOutboundsResolveDNS, v => v.cmbSBResolverDNS.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.SingboxFinalResolveDNS, v => v.cmbSBFinalResolverDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RayStrategy4Freedom, v => v.cmbRayFreedomDNSStrategy.SelectedItem).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.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.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.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); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
@ -56,27 +57,25 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.Bind(ViewModel, vm => vm.UseSystemHostsCompatible, v => v.togUseSystemHostsCompatible.IsChecked).DisposeWith(disposables); 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.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.NormalDNSCompatible, v => v.txtnormalDNSCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainStrategy4Freedom2Compatible, v => v.cmbdomainStrategy4OutCompatible.SelectedItem).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.NormalDNS2Compatible, v => v.txtnormalDNS2Compatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunDNS2Compatible, v => v.txttunDNS2Compatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunDNS2Compatible, v => v.txttunDNS2Compatible.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables);
this.WhenAnyValue( this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
x => x.ViewModel.RayCustomDNSEnableCompatible, .Select(b => !b)
x => x.ViewModel.SBCustomDNSEnableCompatible, .BindTo(this.FindControl<TextBlock>("txtBasicDNSSettingsInvalid"), t => t.IsVisible);
(ray, sb) => ray && sb this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
).BindTo(this.FindControl<TextBlock>("txtBasicDNSSettingsInvalid"), t => t.IsVisible); .Select(b => !b)
this.WhenAnyValue( .BindTo(this.FindControl<TextBlock>("txtAdvancedDNSSettingsInvalid"), t => t.IsVisible);
x => x.ViewModel.RayCustomDNSEnableCompatible, this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridBasicDNSSettings.IsEnabled).DisposeWith(disposables);
x => x.ViewModel.SBCustomDNSEnableCompatible, this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridAdvancedDNSSettings.IsEnabled).DisposeWith(disposables);
(ray, sb) => ray && sb
).BindTo(this.FindControl<TextBlock>("txtAdvancedDNSSettingsInvalid"), t => t.IsVisible);
}); });
} }

View file

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

View file

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

View file

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

View file

@ -75,6 +75,7 @@
Width="{StaticResource IconButtonWidth}" Width="{StaticResource IconButtonWidth}"
Height="{StaticResource IconButtonHeight}" Height="{StaticResource IconButtonHeight}"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
HorizontalAlignment="Left"
Theme="{DynamicResource BorderlessButton}"> Theme="{DynamicResource BorderlessButton}">
<Button.Content> <Button.Content>
<PathIcon Data="{StaticResource building_more}" Foreground="{DynamicResource ButtonDefaultTertiaryForeground}" /> <PathIcon Data="{StaticResource building_more}" Foreground="{DynamicResource ButtonDefaultTertiaryForeground}" />
@ -208,8 +209,8 @@
Grid.Row="9" Grid.Row="9"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" Click="BtnSelectPrevProfile_Click"
Click="BtnSelectPrevProfile_Click" /> Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock <TextBlock
Grid.Row="10" Grid.Row="10"
@ -228,8 +229,8 @@
Grid.Row="10" Grid.Row="10"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" Click="BtnSelectNextProfile_Click"
Click="BtnSelectNextProfile_Click" /> Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock <TextBlock
Grid.Row="11" Grid.Row="11"

View file

@ -41,7 +41,7 @@
<TabControl HorizontalContentAlignment="Left"> <TabControl HorizontalContentAlignment="Left">
<TabItem Header="{x:Static resx:ResUI.ThBasicDNSSettings}"> <TabItem Header="{x:Static resx:ResUI.ThBasicDNSSettings}">
<ScrollViewer VerticalScrollBarVisibility="Visible"> <ScrollViewer VerticalScrollBarVisibility="Visible">
<Grid Margin="{StaticResource Margin8}"> <Grid x:Name="gridBasicDNSSettings" Margin="{StaticResource Margin8}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@ -131,7 +131,7 @@
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSBDoHResolverServer}" /> Text="{x:Static resx:ResUI.TbSBBootstrapDNS}" />
<ComboBox <ComboBox
x:Name="cmbSBFinalResolverDNS" x:Name="cmbSBFinalResolverDNS"
Grid.Row="4" Grid.Row="4"
@ -210,19 +210,12 @@
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="3"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSBDoHOverride}" />
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<TabItem Header="{x:Static resx:ResUI.ThAdvancedDNSSettings}"> <TabItem Header="{x:Static resx:ResUI.ThAdvancedDNSSettings}">
<ScrollViewer VerticalScrollBarVisibility="Visible"> <ScrollViewer VerticalScrollBarVisibility="Visible">
<Grid Margin="{StaticResource Margin8}"> <Grid x:Name="gridAdvancedDNSSettings" Margin="{StaticResource Margin8}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />

View file

@ -1,4 +1,5 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows; using System.Windows;
using ReactiveUI; using ReactiveUI;
@ -65,18 +66,16 @@ public partial class DNSSettingWindow
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables);
this.WhenAnyValue( this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
x => x.ViewModel.RayCustomDNSEnableCompatible, .Select(b => b ? Visibility.Collapsed : Visibility.Visible)
x => x.ViewModel.SBCustomDNSEnableCompatible,
(ray, sb) => ray && sb ? Visibility.Visible : Visibility.Collapsed)
.BindTo(this, x => x.txtBasicDNSSettingsInvalid.Visibility) .BindTo(this, x => x.txtBasicDNSSettingsInvalid.Visibility)
.DisposeWith(disposables); .DisposeWith(disposables);
this.WhenAnyValue( this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
x => x.ViewModel.RayCustomDNSEnableCompatible, .Select(b => b ? Visibility.Collapsed : Visibility.Visible)
x => x.ViewModel.SBCustomDNSEnableCompatible,
(ray, sb) => ray && sb ? Visibility.Visible : Visibility.Collapsed)
.BindTo(this, x => x.txtAdvancedDNSSettingsInvalid.Visibility) .BindTo(this, x => x.txtAdvancedDNSSettingsInvalid.Visibility)
.DisposeWith(disposables); .DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridBasicDNSSettings.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridAdvancedDNSSettings.IsEnabled).DisposeWith(disposables);
}); });
WindowsUtils.SetDarkBorder(this, AppManager.Instance.Config.UiItem.CurrentTheme); WindowsUtils.SetDarkBorder(this, AppManager.Instance.Config.UiItem.CurrentTheme);
} }

View file

@ -117,7 +117,7 @@
<materialDesign:PopupBox <materialDesign:PopupBox
Grid.Row="2" Grid.Row="2"
Grid.Column="2" Grid.Column="2"
HorizontalAlignment="Right" HorizontalAlignment="Left"
StaysOpen="True" StaysOpen="True"
Style="{StaticResource MaterialDesignToolForegroundPopupBox}"> Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
<StackPanel> <StackPanel>