Compare commits

...

7 commits

Author SHA1 Message Date
DHR60
cf503da469 Avoids detour for private networks 2025-08-10 11:28:00 +08:00
DHR60
9ec6d3f955 Add Detour Feature 2025-08-10 11:28:00 +08:00
DHR60
e357e2a0b6 Adjust Avalonia UI 2025-08-10 11:28:00 +08:00
DHR60
237eba22e4 Fixes 2025-08-10 11:28:00 +08:00
DHR60
856d155937 Adjust UI 2025-08-10 11:28:00 +08:00
DHR60
ff1df89038 Fixes TypeInfoResolver Exception 2025-08-10 11:27:59 +08:00
DHR60
6349553fe0 Feat. custom config 2025-08-10 11:27:59 +08:00
23 changed files with 1153 additions and 5 deletions

View file

@ -29,6 +29,7 @@ public enum EViewAction
DNSSettingWindow, DNSSettingWindow,
RoutingSettingWindow, RoutingSettingWindow,
OptionSettingWindow, OptionSettingWindow,
CustomConfigWindow,
GlobalHotkeySettingWindow, GlobalHotkeySettingWindow,
SubSettingWindow, SubSettingWindow,
DispatcherSpeedTest, DispatcherSpeedTest,

View file

@ -64,6 +64,7 @@ public sealed class AppHandler
SQLiteHelper.Instance.CreateTable<RoutingItem>(); SQLiteHelper.Instance.CreateTable<RoutingItem>();
SQLiteHelper.Instance.CreateTable<ProfileExItem>(); SQLiteHelper.Instance.CreateTable<ProfileExItem>();
SQLiteHelper.Instance.CreateTable<DNSItem>(); SQLiteHelper.Instance.CreateTable<DNSItem>();
SQLiteHelper.Instance.CreateTable<CustomConfigItem>();
return true; return true;
} }
@ -203,6 +204,16 @@ public sealed class AppHandler
return await SQLiteHelper.Instance.TableAsync<DNSItem>().FirstOrDefaultAsync(it => it.CoreType == eCoreType); return await SQLiteHelper.Instance.TableAsync<DNSItem>().FirstOrDefaultAsync(it => it.CoreType == eCoreType);
} }
public async Task<List<CustomConfigItem>?> CustomConfigItem()
{
return await SQLiteHelper.Instance.TableAsync<CustomConfigItem>().ToListAsync();
}
public async Task<CustomConfigItem?> GetCustomConfigItem(ECoreType eCoreType)
{
return await SQLiteHelper.Instance.TableAsync<CustomConfigItem>().FirstOrDefaultAsync(it => it.CoreType == eCoreType);
}
#endregion SqliteHelper #endregion SqliteHelper
#region Core Type #region Core Type

View file

@ -2184,6 +2184,54 @@ public class ConfigHandler
#endregion DNS #endregion DNS
#region Custom Config
public static async Task<int> InitBuiltinCustomConfig(Config config)
{
var items = await AppHandler.Instance.CustomConfigItem();
if (items.Count <= 0)
{
var item = new CustomConfigItem()
{
Remarks = "V2ray",
CoreType = ECoreType.Xray,
};
await SaveCustomConfigItem(config, item);
var item2 = new CustomConfigItem()
{
Remarks = "sing-box",
CoreType = ECoreType.sing_box,
};
await SaveCustomConfigItem(config, item2);
}
return 0;
}
public static async Task<int> SaveCustomConfigItem(Config config, CustomConfigItem item)
{
if (item == null)
{
return -1;
}
if (item.Id.IsNullOrEmpty())
{
item.Id = Utils.GetGuid(false);
}
if (await SQLiteHelper.Instance.ReplaceAsync(item) > 0)
{
return 0;
}
else
{
return -1;
}
}
#endregion Custom Config
#region Regional Presets #region Regional Presets
/// <summary> /// <summary>

View file

@ -0,0 +1,18 @@
using SQLite;
namespace ServiceLib.Models;
[Serializable]
public class CustomConfigItem
{
[PrimaryKey]
public string Id { get; set; }
public string Remarks { get; set; }
public bool Enabled { get; set; } = false;
public ECoreType CoreType { get; set; }
public string? Config { get; set; }
public string? TunConfig { get; set; }
public bool? AddProxyOnly { get; set; } = false;
public string? ProxyDetour { get; set; }
}

View file

@ -1,4 +1,4 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// <auto-generated> // <auto-generated>
// 此代码由工具生成。 // 此代码由工具生成。
// 运行时版本:4.0.30319.42000 // 运行时版本:4.0.30319.42000
@ -186,6 +186,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Please fill in the correct custom config 的本地化字符串。
/// </summary>
public static string FillCorrectConfigText {
get {
return ResourceManager.GetString("FillCorrectConfigText", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Please fill in the correct custom DNS 的本地化字符串。 /// 查找类似 Please fill in the correct custom DNS 的本地化字符串。
/// </summary> /// </summary>
@ -852,6 +861,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom Config 的本地化字符串。
/// </summary>
public static string menuCustomConfig {
get {
return ResourceManager.GetString("menuCustomConfig", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 DNS Settings 的本地化字符串。 /// 查找类似 DNS Settings 的本地化字符串。
/// </summary> /// </summary>
@ -2220,6 +2238,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Do Not Add Non-Proxy Protocol Outbound 的本地化字符串。
/// </summary>
public static string TbAddProxyProtocolOutboundOnly {
get {
return ResourceManager.GetString("TbAddProxyProtocolOutboundOnly", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Address 的本地化字符串。 /// 查找类似 Address 的本地化字符串。
/// </summary> /// </summary>
@ -2337,6 +2364,42 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Enable Custom Config 的本地化字符串。
/// </summary>
public static string TbCustomConfigEnable {
get {
return ResourceManager.GetString("TbCustomConfigEnable", resourceCulture);
}
}
/// <summary>
/// 查找类似 sing-box Custom Config 的本地化字符串。
/// </summary>
public static string TbCustomConfigSingbox {
get {
return ResourceManager.GetString("TbCustomConfigSingbox", resourceCulture);
}
}
/// <summary>
/// 查找类似 Enable Custom DNS 的本地化字符串。
/// </summary>
public static string TbCustomDNSEnable {
get {
return ResourceManager.GetString("TbCustomDNSEnable", resourceCulture);
}
}
/// <summary>
/// 查找类似 Custom DNS Enabled, This Page&apos;s Settings Invalid 的本地化字符串。
/// </summary>
public static string TbCustomDNSEnabledPageInvalid {
get {
return ResourceManager.GetString("TbCustomDNSEnabledPageInvalid", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Display GUI 的本地化字符串。 /// 查找类似 Display GUI 的本地化字符串。
/// </summary> /// </summary>
@ -2643,6 +2706,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 v2ray Custom Config 的本地化字符串。
/// </summary>
public static string TbRayCustomConfig {
get {
return ResourceManager.GetString("TbRayCustomConfig", resourceCulture);
}
}
/// <summary>
/// 查找类似 Add Outbound Config Only, routing.balancers and routing.rules.outboundTag 的本地化字符串。
/// </summary>
public static string TbRayCustomConfigDesc {
get {
return ResourceManager.GetString("TbRayCustomConfigDesc", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Alias (remarks) 的本地化字符串。 /// 查找类似 Alias (remarks) 的本地化字符串。
/// </summary> /// </summary>
@ -2760,6 +2841,78 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Add Outbound and Endpoint Config Only 的本地化字符串。
/// </summary>
public static string TbSBCustomConfigDesc {
get {
return ResourceManager.GetString("TbSBCustomConfigDesc", resourceCulture);
}
}
/// <summary>
/// 查找类似 sing-box Direct Resolution Strategy 的本地化字符串。
/// </summary>
public static string TbSBDirectResolveStrategy {
get {
return ResourceManager.GetString("TbSBDirectResolveStrategy", resourceCulture);
}
}
/// <summary>
/// 查找类似 The sing-box DoH resolution server can be overwritten 的本地化字符串。
/// </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>
public static string TbSBFallbackDNSResolve {
get {
return ResourceManager.GetString("TbSBFallbackDNSResolve", resourceCulture);
}
}
/// <summary>
/// 查找类似 Resolve Outbound Domains 的本地化字符串。
/// </summary>
public static string TbSBOutboundDomainResolve {
get {
return ResourceManager.GetString("TbSBOutboundDomainResolve", resourceCulture);
}
}
/// <summary>
/// 查找类似 Outbound DNS Resolution (sing-box) 的本地化字符串。
/// </summary>
public static string TbSBOutboundsResolverDNS {
get {
return ResourceManager.GetString("TbSBOutboundsResolverDNS", resourceCulture);
}
}
/// <summary>
/// 查找类似 sing-box Remote Resolution Strategy 的本地化字符串。
/// </summary>
public static string TbSBRemoteResolveStrategy {
get {
return ResourceManager.GetString("TbSBRemoteResolveStrategy", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Encryption method (security) 的本地化字符串。 /// 查找类似 Encryption method (security) 的本地化字符串。
/// </summary> /// </summary>
@ -3543,6 +3696,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Set Upstream Proxy Tag 的本地化字符串。
/// </summary>
public static string TbSetUpstreamProxyDetour {
get {
return ResourceManager.GetString("TbSetUpstreamProxyDetour", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Short Id 的本地化字符串。 /// 查找类似 Short Id 的本地化字符串。
/// </summary> /// </summary>

View file

@ -1404,4 +1404,28 @@
<data name="menuAddAnytlsServer" xml:space="preserve"> <data name="menuAddAnytlsServer" xml:space="preserve">
<value>Add [Anytls] Configuration</value> <value>Add [Anytls] Configuration</value>
</data> </data>
<data name="menuCustomConfig" xml:space="preserve">
<value>Custom Config</value>
</data>
<data name="TbCustomConfigEnable" xml:space="preserve">
<value>Enable Custom Config</value>
</data>
<data name="TbRayCustomConfig" xml:space="preserve">
<value>v2ray Custom Config</value>
</data>
<data name="TbCustomConfigSingbox" xml:space="preserve">
<value>sing-box Custom Config</value>
</data>
<data name="TbRayCustomConfigDesc" xml:space="preserve">
<value>Add Outbound Config Only, routing.balancers and routing.rules.outboundTag</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Add Outbound and Endpoint Config Only</value>
</data>
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
<value>Do Not Add Non-Proxy Protocol Outbound</value>
</data>
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
<value>Set Upstream Proxy Tag</value>
</data>
</root> </root>

View file

@ -1404,4 +1404,28 @@
<data name="menuAddAnytlsServer" xml:space="preserve"> <data name="menuAddAnytlsServer" xml:space="preserve">
<value>[Anytls] konfiguráció hozzáadása</value> <value>[Anytls] konfiguráció hozzáadása</value>
</data> </data>
<data name="menuCustomConfig" xml:space="preserve">
<value>Custom Config</value>
</data>
<data name="TbCustomConfigEnable" xml:space="preserve">
<value>Enable Custom Config</value>
</data>
<data name="TbRayCustomConfig" xml:space="preserve">
<value>v2ray Custom Config</value>
</data>
<data name="TbCustomConfigSingbox" xml:space="preserve">
<value>sing-box Custom Config</value>
</data>
<data name="TbRayCustomConfigDesc" xml:space="preserve">
<value>Add Outbound Config Only, routing.balancers and routing.rules.outboundTag</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Add Outbound and Endpoint Config Only</value>
</data>
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
<value>Do Not Add Non-Proxy Protocol Outbound</value>
</data>
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
<value>Set Upstream Proxy Tag</value>
</data>
</root> </root>

View file

@ -1404,4 +1404,28 @@
<data name="menuAddAnytlsServer" xml:space="preserve"> <data name="menuAddAnytlsServer" xml:space="preserve">
<value>Add [Anytls] Configuration</value> <value>Add [Anytls] Configuration</value>
</data> </data>
<data name="menuCustomConfig" xml:space="preserve">
<value>Custom Config</value>
</data>
<data name="TbCustomConfigEnable" xml:space="preserve">
<value>Enable Custom Config</value>
</data>
<data name="TbRayCustomConfig" xml:space="preserve">
<value>v2ray Custom Config</value>
</data>
<data name="TbCustomConfigSingbox" xml:space="preserve">
<value>sing-box Custom Config</value>
</data>
<data name="TbRayCustomConfigDesc" xml:space="preserve">
<value>Add Outbound Config Only, routing.balancers and routing.rules.outboundTag</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Add Outbound and Endpoint Config Only</value>
</data>
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
<value>Do Not Add Non-Proxy Protocol Outbound</value>
</data>
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
<value>Set Upstream Proxy Tag</value>
</data>
</root> </root>

View file

@ -1404,4 +1404,28 @@
<data name="menuAddAnytlsServer" xml:space="preserve"> <data name="menuAddAnytlsServer" xml:space="preserve">
<value>Добавить сервер [Anytls]</value> <value>Добавить сервер [Anytls]</value>
</data> </data>
<data name="menuCustomConfig" xml:space="preserve">
<value>Custom Config</value>
</data>
<data name="TbCustomConfigEnable" xml:space="preserve">
<value>Enable Custom Config</value>
</data>
<data name="TbRayCustomConfig" xml:space="preserve">
<value>v2ray Custom Config</value>
</data>
<data name="TbCustomConfigSingbox" xml:space="preserve">
<value>sing-box Custom Config</value>
</data>
<data name="TbRayCustomConfigDesc" xml:space="preserve">
<value>Add Outbound Config Only, routing.balancers and routing.rules.outboundTag</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Add Outbound and Endpoint Config Only</value>
</data>
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
<value>Do Not Add Non-Proxy Protocol Outbound</value>
</data>
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
<value>Set Upstream Proxy Tag</value>
</data>
</root> </root>

View file

@ -1401,4 +1401,28 @@
<data name="menuAddAnytlsServer" xml:space="preserve"> <data name="menuAddAnytlsServer" xml:space="preserve">
<value>添加 [Anytls] 配置文件</value> <value>添加 [Anytls] 配置文件</value>
</data> </data>
<data name="menuCustomConfig" xml:space="preserve">
<value>自定义配置</value>
</data>
<data name="TbCustomConfigEnable" xml:space="preserve">
<value>启用自定义配置</value>
</data>
<data name="TbRayCustomConfig" xml:space="preserve">
<value>v2ray 自定义配置</value>
</data>
<data name="TbCustomConfigSingbox" xml:space="preserve">
<value>sing-box 自定义配置</value>
</data>
<data name="TbRayCustomConfigDesc" xml:space="preserve">
<value>仅添加出站配置routing.balancers 和 routing.rules.outboundTag</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>仅添加出站和端点配置</value>
</data>
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
<value>不添加非代理协议出站</value>
</data>
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
<value>设置上游代理 tag</value>
</data>
</root> </root>

View file

@ -1401,4 +1401,28 @@
<data name="menuAddAnytlsServer" xml:space="preserve"> <data name="menuAddAnytlsServer" xml:space="preserve">
<value>新增 [Anytls] 設定檔</value> <value>新增 [Anytls] 設定檔</value>
</data> </data>
<data name="menuCustomConfig" xml:space="preserve">
<value>Custom Config</value>
</data>
<data name="TbCustomConfigEnable" xml:space="preserve">
<value>Enable Custom Config</value>
</data>
<data name="TbRayCustomConfig" xml:space="preserve">
<value>v2ray Custom Config</value>
</data>
<data name="TbCustomConfigSingbox" xml:space="preserve">
<value>sing-box Custom Config</value>
</data>
<data name="TbRayCustomConfigDesc" xml:space="preserve">
<value>Add Outbound Config Only, routing.balancers and routing.rules.outboundTag</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Add Outbound and Endpoint Config Only</value>
</data>
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
<value>Do Not Add Non-Proxy Protocol Outbound</value>
</data>
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
<value>Set Upstream Proxy Tag</value>
</data>
</root> </root>

View file

@ -2,6 +2,8 @@ using System.Data;
using System.Net; using System.Net;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Reactive; using System.Reactive;
using System.Text;
using System.Text.Json.Nodes;
using DynamicData; using DynamicData;
using ServiceLib.Models; using ServiceLib.Models;
@ -81,7 +83,9 @@ public class CoreConfigSingboxService
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(singboxConfig);
var customConfig = await AppHandler.Instance.GetCustomConfigItem(ECoreType.sing_box);
ret.Data = await ApplyCustomConfig(customConfig, singboxConfig);
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@ -421,7 +425,9 @@ public class CoreConfigSingboxService
await ConvertGeo2Ruleset(singboxConfig); await ConvertGeo2Ruleset(singboxConfig);
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(singboxConfig);
var customConfig = await AppHandler.Instance.GetCustomConfigItem(ECoreType.sing_box);
ret.Data = await ApplyCustomConfig(customConfig, singboxConfig);
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@ -1970,5 +1976,61 @@ public class CoreConfigSingboxService
return 0; return 0;
} }
private async Task<string> ApplyCustomConfig(CustomConfigItem customConfig, SingboxConfig singboxConfig)
{
var customConfigItem = customConfig.Config;
if (_config.TunModeItem.EnableTun)
{
customConfigItem = customConfig.TunConfig;
}
if (!customConfig.Enabled || customConfigItem.IsNullOrEmpty())
{
return JsonUtils.Serialize(singboxConfig);
}
var customConfigNode = JsonNode.Parse(customConfigItem);
if (customConfigNode == null)
{
return JsonUtils.Serialize(singboxConfig);
}
// Process outbounds
var customOutboundsNode = customConfigNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
foreach (var outbound in singboxConfig.outbounds)
{
if (outbound.type.ToLower() is "direct" or "block")
{
if (customConfig.AddProxyOnly == true)
{
continue;
}
}
else if (outbound.detour.IsNullOrEmpty() && (!customConfig.ProxyDetour.IsNullOrEmpty()) && !Utils.IsPrivateNetwork(outbound.server ?? string.Empty))
{
outbound.detour = customConfig.ProxyDetour;
}
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
}
customConfigNode["outbounds"] = customOutboundsNode;
// Process endpoints
if (singboxConfig.endpoints != null && singboxConfig.endpoints.Count > 0)
{
var customEndpointsNode = customConfigNode["endpoints"] is JsonArray endpoints ? endpoints : new JsonArray();
foreach (var endpoint in singboxConfig.endpoints)
{
if (endpoint.detour.IsNullOrEmpty() && (!customConfig.ProxyDetour.IsNullOrEmpty()))
{
endpoint.detour = customConfig.ProxyDetour;
}
customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint));
}
customConfigNode["endpoints"] = customEndpointsNode;
}
return await Task.FromResult(JsonUtils.Serialize(customConfigNode));
}
#endregion private gen function #endregion private gen function
} }

View file

@ -1,6 +1,8 @@
using System.Net; using System.Net;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ServiceLib.Models;
namespace ServiceLib.Services.CoreConfig; namespace ServiceLib.Services.CoreConfig;
@ -66,7 +68,9 @@ public class CoreConfigV2rayService
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(v2rayConfig);
var customConfig = await AppHandler.Instance.GetCustomConfigItem(ECoreType.Xray);
ret.Data = await ApplyCustomConfig(customConfig, v2rayConfig);
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@ -195,7 +199,9 @@ public class CoreConfigV2rayService
} }
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(v2rayConfig);
var customConfig = await AppHandler.Instance.GetCustomConfigItem(ECoreType.Xray);
ret.Data = await ApplyCustomConfig(customConfig, v2rayConfig, true);
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@ -1570,5 +1576,83 @@ public class CoreConfigV2rayService
return await Task.FromResult(0); return await Task.FromResult(0);
} }
private async Task<string> ApplyCustomConfig(CustomConfigItem customConfig, V2rayConfig v2rayConfig, bool handleBalancerAndRules = false)
{
if (!customConfig.Enabled || customConfig.Config.IsNullOrEmpty())
{
return JsonUtils.Serialize(v2rayConfig);
}
var customConfigNode = JsonNode.Parse(customConfig.Config);
if (customConfigNode == null)
{
return JsonUtils.Serialize(v2rayConfig);
}
// Handle balancer and rules modifications (for multiple load scenarios)
if (handleBalancerAndRules && v2rayConfig.routing?.balancers?.Count > 0)
{
var balancer = v2rayConfig.routing.balancers.First();
// Modify existing rules in custom config
var rulesNode = customConfigNode["routing"]?["rules"];
if (rulesNode != null)
{
foreach (var rule in rulesNode.AsArray())
{
if (rule["outboundTag"]?.GetValue<string>() == Global.ProxyTag)
{
rule.AsObject().Remove("outboundTag");
rule["balancerTag"] = balancer.tag;
}
}
}
// Ensure routing node exists
if (customConfigNode["routing"] == null)
{
customConfigNode["routing"] = new JsonObject();
}
// Handle balancers - append instead of override
if (customConfigNode["routing"]["balancers"] is JsonArray customBalancersNode)
{
if (JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers)) is JsonArray newBalancers)
{
foreach (var balancerNode in newBalancers)
{
customBalancersNode.Add(balancerNode?.DeepClone());
}
}
}
else
{
customConfigNode["routing"]["balancers"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers));
}
}
// Handle outbounds - append instead of override
var customOutboundsNode = customConfigNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
foreach (var outbound in v2rayConfig.outbounds)
{
if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom")
{
if (customConfig.AddProxyOnly == true)
{
continue;
}
}
else if ((outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() == true) && (!customConfig.ProxyDetour.IsNullOrEmpty()) && !(Utils.IsPrivateNetwork(outbound.settings?.servers?.FirstOrDefault()?.address ?? string.Empty) || Utils.IsPrivateNetwork(outbound.settings?.vnext?.FirstOrDefault()?.address ?? string.Empty)))
{
outbound.streamSettings ??= new StreamSettings4Ray();
outbound.streamSettings.sockopt ??= new Sockopt4Ray();
outbound.streamSettings.sockopt.dialerProxy = customConfig.ProxyDetour;
}
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
}
return await Task.FromResult(JsonUtils.Serialize(customConfigNode));
}
#endregion private gen function #endregion private gen function
} }

View file

@ -0,0 +1,110 @@
using System.Reactive;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace ServiceLib.ViewModels;
public class CustomConfigViewModel : MyReactiveObject
{
#region Reactive
[Reactive]
public bool EnableCustomConfig4Ray { get; set; }
[Reactive]
public bool EnableCustomConfig4Singbox { get; set; }
[Reactive]
public string CustomConfig4Ray { get; set; }
[Reactive]
public string CustomConfig4Singbox { get; set; }
[Reactive]
public string CustomTunConfig4Singbox { get; set; }
[Reactive]
public bool AddProxyOnly4Ray { get; set; }
[Reactive]
public bool AddProxyOnly4Singbox { get; set; }
[Reactive]
public string ProxyDetour4Ray { get; set; }
[Reactive]
public string ProxyDetour4Singbox { get; set; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
#endregion Reactive
public CustomConfigViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{
_config = AppHandler.Instance.Config;
_updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SaveSettingAsync();
});
_ = Init();
}
private async Task Init()
{
var item = await AppHandler.Instance.GetCustomConfigItem(ECoreType.Xray);
EnableCustomConfig4Ray = item?.Enabled ?? false;
CustomConfig4Ray = item?.Config ?? string.Empty;
AddProxyOnly4Ray = item?.AddProxyOnly ?? false;
ProxyDetour4Ray = item?.ProxyDetour ?? string.Empty;
var item2 = await AppHandler.Instance.GetCustomConfigItem(ECoreType.sing_box);
EnableCustomConfig4Singbox = item2?.Enabled ?? false;
CustomConfig4Singbox = item2?.Config ?? string.Empty;
CustomTunConfig4Singbox = item2?.TunConfig ?? string.Empty;
AddProxyOnly4Singbox = item2?.AddProxyOnly ?? false;
ProxyDetour4Singbox = item2?.ProxyDetour ?? string.Empty;
}
private async Task SaveSettingAsync()
{
if (!await SaveXrayConfigAsync())
return;
if (!await SaveSingboxConfigAsync())
return;
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess);
_ = _updateView?.Invoke(EViewAction.CloseWindow, null);
}
private async Task<bool> SaveXrayConfigAsync()
{
var item = await AppHandler.Instance.GetCustomConfigItem(ECoreType.Xray);
item.Enabled = EnableCustomConfig4Ray;
item.Config = null;
item.Config = CustomConfig4Ray;
item.AddProxyOnly = AddProxyOnly4Ray;
item.ProxyDetour = ProxyDetour4Ray;
await ConfigHandler.SaveCustomConfigItem(_config, item);
return true;
}
private async Task<bool> SaveSingboxConfigAsync()
{
var item = await AppHandler.Instance.GetCustomConfigItem(ECoreType.sing_box);
item.Enabled = EnableCustomConfig4Singbox;
item.Config = null;
item.TunConfig = null;
item.Config = CustomConfig4Singbox;
item.TunConfig = CustomTunConfig4Singbox;
item.AddProxyOnly = AddProxyOnly4Singbox;
item.ProxyDetour = ProxyDetour4Singbox;
await ConfigHandler.SaveCustomConfigItem(_config, item);
return true;
}
}

View file

@ -39,6 +39,7 @@ public class MainWindowViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> RoutingSettingCmd { get; } public ReactiveCommand<Unit, Unit> RoutingSettingCmd { get; }
public ReactiveCommand<Unit, Unit> DNSSettingCmd { get; } public ReactiveCommand<Unit, Unit> DNSSettingCmd { get; }
public ReactiveCommand<Unit, Unit> CustomConfigCmd { get; }
public ReactiveCommand<Unit, Unit> GlobalHotkeySettingCmd { get; } public ReactiveCommand<Unit, Unit> GlobalHotkeySettingCmd { get; }
public ReactiveCommand<Unit, Unit> RebootAsAdminCmd { get; } public ReactiveCommand<Unit, Unit> RebootAsAdminCmd { get; }
public ReactiveCommand<Unit, Unit> ClearServerStatisticsCmd { get; } public ReactiveCommand<Unit, Unit> ClearServerStatisticsCmd { get; }
@ -169,6 +170,10 @@ public class MainWindowViewModel : MyReactiveObject
{ {
await DNSSettingAsync(); await DNSSettingAsync();
}); });
CustomConfigCmd = ReactiveCommand.CreateFromTask(async () =>
{
await CustomConfigAsync();
});
GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () => GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true) if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true)
@ -220,6 +225,7 @@ public class MainWindowViewModel : MyReactiveObject
await ConfigHandler.InitBuiltinRouting(_config); await ConfigHandler.InitBuiltinRouting(_config);
await ConfigHandler.InitBuiltinDNS(_config); await ConfigHandler.InitBuiltinDNS(_config);
await ConfigHandler.InitBuiltinCustomConfig(_config);
await ProfileExHandler.Instance.Init(); await ProfileExHandler.Instance.Init();
await CoreHandler.Instance.Init(_config, UpdateHandler); await CoreHandler.Instance.Init(_config, UpdateHandler);
TaskHandler.Instance.RegUpdateTask(_config, UpdateTaskHandler); TaskHandler.Instance.RegUpdateTask(_config, UpdateTaskHandler);
@ -508,6 +514,15 @@ public class MainWindowViewModel : MyReactiveObject
} }
} }
private async Task CustomConfigAsync()
{
var ret = await _updateView?.Invoke(EViewAction.CustomConfigWindow, null);
if (ret == true)
{
await Reload();
}
}
public async Task RebootAsAdmin() public async Task RebootAsAdmin()
{ {
ProcUtils.RebootAsAdmin(); ProcUtils.RebootAsAdmin();

View file

@ -0,0 +1,182 @@
<Window
x:Class="v2rayN.Desktop.Views.CustomConfigWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Title="{x:Static resx:ResUI.menuCustomConfig}"
Width="900"
Height="600"
x:DataType="vms:CustomConfigViewModel"
ShowInTaskbar="False"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<DockPanel Margin="{StaticResource Margin8}">
<StackPanel
Margin="{StaticResource Margin4}"
HorizontalAlignment="Center"
DockPanel.Dock="Bottom"
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
Content="{x:Static resx:ResUI.TbConfirm}"
Cursor="Hand"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
Cursor="Hand"
IsCancel="True" />
</StackPanel>
<TabControl HorizontalContentAlignment="Stretch">
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.TbRayCustomConfig}">
<DockPanel Margin="{StaticResource Margin4}">
<Grid DockPanel.Dock="Top" RowDefinitions="Auto,Auto">
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock
Grid.Row="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRayCustomConfigDesc}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCustomConfigEnable}" />
<ToggleSwitch
x:Name="rayCustomConfigEnable"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
</StackPanel>
</Grid>
<WrapPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbAddProxyProtocolOutboundOnly}" />
<ToggleSwitch
x:Name="togAddProxyProtocolOutboundOnly4Ray"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSetUpstreamProxyDetour}" />
<TextBox
x:Name="txtProxyDetour4Ray"
Width="200"
Margin="{StaticResource Margin4}" />
</StackPanel>
</WrapPanel>
<HeaderedContentControl
Margin="{StaticResource Margin4}"
BorderBrush="Gray"
BorderThickness="1"
Header="xray json config">
<TextBox
x:Name="rayCustomConfig"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
</HeaderedContentControl>
</DockPanel>
</TabItem>
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.TbCustomConfigSingbox}">
<DockPanel Margin="{StaticResource Margin4}">
<Grid DockPanel.Dock="Top" RowDefinitions="Auto,Auto">
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock
Grid.Row="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBCustomConfigDesc}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCustomConfigEnable}" />
<ToggleSwitch
x:Name="sbCustomConfigEnable"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
</StackPanel>
</Grid>
<WrapPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbAddProxyProtocolOutboundOnly}" />
<ToggleSwitch
x:Name="togAddProxyProtocolOutboundOnly4Singbox"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSetUpstreamProxyDetour}" />
<TextBox
x:Name="txtProxyDetour4Singbox"
Width="200"
Margin="{StaticResource Margin4}" />
</StackPanel>
</WrapPanel>
<Grid Margin="{StaticResource Margin4}" ColumnDefinitions="*,10,*">
<HeaderedContentControl
Grid.Column="0"
BorderBrush="Gray"
BorderThickness="1"
Header="sing-box json config">
<TextBox
x:Name="sbCustomConfig"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
</HeaderedContentControl>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" />
<HeaderedContentControl
Grid.Column="2"
BorderBrush="Gray"
BorderThickness="1"
Header="sing-box json tun config">
<TextBox
x:Name="sbCustomTunConfig"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
</HeaderedContentControl>
</Grid>
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>

View file

@ -0,0 +1,46 @@
using System.Reactive.Disposables;
using Avalonia.Interactivity;
using ReactiveUI;
using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views;
public partial class CustomConfigWindow : WindowBase<CustomConfigViewModel>
{
private static Config _config;
public CustomConfigWindow()
{
InitializeComponent();
_config = AppHandler.Instance.Config;
btnCancel.Click += (s, e) => this.Close();
ViewModel = new CustomConfigViewModel(UpdateViewHandler);
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.EnableCustomConfig4Ray, v => v.rayCustomConfigEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomConfig4Ray, v => v.rayCustomConfig.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AddProxyOnly4Ray, v => v.togAddProxyProtocolOutboundOnly4Ray.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ProxyDetour4Ray, v => v.txtProxyDetour4Ray.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCustomConfig4Singbox, v => v.sbCustomConfigEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomConfig4Singbox, v => v.sbCustomConfig.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomTunConfig4Singbox, v => v.sbCustomTunConfig.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AddProxyOnly4Singbox, v => v.togAddProxyProtocolOutboundOnly4Singbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ProxyDetour4Singbox, v => v.txtProxyDetour4Singbox.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
this.Close(true);
break;
}
return await Task.FromResult(true);
}
}

View file

@ -72,6 +72,7 @@
<MenuItem x:Name="menuOptionSetting" Header="{x:Static resx:ResUI.menuOptionSetting}" /> <MenuItem x:Name="menuOptionSetting" Header="{x:Static resx:ResUI.menuOptionSetting}" />
<MenuItem x:Name="menuRoutingSetting" Header="{x:Static resx:ResUI.menuRoutingSetting}" /> <MenuItem x:Name="menuRoutingSetting" Header="{x:Static resx:ResUI.menuRoutingSetting}" />
<MenuItem x:Name="menuDNSSetting" Header="{x:Static resx:ResUI.menuDNSSetting}" /> <MenuItem x:Name="menuDNSSetting" Header="{x:Static resx:ResUI.menuDNSSetting}" />
<MenuItem x:Name="menuCustomConfig" Header="{x:Static resx:ResUI.menuCustomConfig}" />
<MenuItem x:Name="menuGlobalHotkeySetting" Header="{x:Static resx:ResUI.menuGlobalHotkeySetting}" /> <MenuItem x:Name="menuGlobalHotkeySetting" Header="{x:Static resx:ResUI.menuGlobalHotkeySetting}" />
<Separator /> <Separator />
<MenuItem x:Name="menuRebootAsAdmin" Header="{x:Static resx:ResUI.menuRebootAsAdmin}" /> <MenuItem x:Name="menuRebootAsAdmin" Header="{x:Static resx:ResUI.menuRebootAsAdmin}" />

View file

@ -100,6 +100,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.BindCommand(ViewModel, vm => vm.OptionSettingCmd, v => v.menuOptionSetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.OptionSettingCmd, v => v.menuOptionSetting).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RoutingSettingCmd, v => v.menuRoutingSetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RoutingSettingCmd, v => v.menuRoutingSetting).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.DNSSettingCmd, v => v.menuDNSSetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.DNSSettingCmd, v => v.menuDNSSetting).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CustomConfigCmd, v => v.menuCustomConfig).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GlobalHotkeySettingCmd, v => v.menuGlobalHotkeySetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GlobalHotkeySettingCmd, v => v.menuGlobalHotkeySetting).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RebootAsAdminCmd, v => v.menuRebootAsAdmin).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RebootAsAdminCmd, v => v.menuRebootAsAdmin).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ClearServerStatisticsCmd, v => v.menuClearServerStatistics).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ClearServerStatisticsCmd, v => v.menuClearServerStatistics).DisposeWith(disposables);
@ -190,6 +191,9 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
case EViewAction.DNSSettingWindow: case EViewAction.DNSSettingWindow:
return await new DNSSettingWindow().ShowDialog<bool>(this); return await new DNSSettingWindow().ShowDialog<bool>(this);
case EViewAction.CustomConfigWindow:
return await new CustomConfigWindow().ShowDialog<bool>(this);
case EViewAction.RoutingSettingWindow: case EViewAction.RoutingSettingWindow:
return await new RoutingSettingWindow().ShowDialog<bool>(this); return await new RoutingSettingWindow().ShowDialog<bool>(this);

View file

@ -0,0 +1,205 @@
<base:WindowBase
x:Class="v2rayN.Views.CustomConfigWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:v2rayN.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveui="http://reactiveui.net"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Title="{x:Static resx:ResUI.menuCustomConfig}"
Width="1000"
Height="700"
x:TypeArguments="vms:CustomConfigViewModel"
ShowInTaskbar="False"
Style="{StaticResource WindowGlobal}"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<DockPanel Margin="{StaticResource Margin8}">
<StackPanel
Margin="{StaticResource Margin4}"
HorizontalAlignment="Center"
DockPanel.Dock="Bottom"
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
Content="{x:Static resx:ResUI.TbConfirm}"
Cursor="Hand"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
Cursor="Hand"
IsCancel="true"
Style="{StaticResource DefButton}" />
</StackPanel>
<TabControl HorizontalContentAlignment="Left">
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.TbRayCustomConfig}">
<DockPanel Margin="{StaticResource Margin8}">
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock
Grid.Row="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbRayCustomConfigDesc}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCustomConfigEnable}" />
<ToggleButton
x:Name="rayCustomConfigEnable"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
</StackPanel>
</Grid>
<WrapPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbAddProxyProtocolOutboundOnly}" />
<ToggleButton
x:Name="togAddProxyProtocolOutboundOnly4Ray"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSetUpstreamProxyDetour}" />
<TextBox
x:Name="txtProxyDetour4Ray"
Width="200"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefTextBox}" />
</StackPanel>
</WrapPanel>
<TextBox
x:Name="rayCustomConfig"
Margin="{StaticResource Margin8}"
VerticalAlignment="Stretch"
materialDesign:HintAssist.Hint="xray json config"
AcceptsReturn="True"
BorderThickness="1"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
</DockPanel>
</TabItem>
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.TbCustomConfigSingbox}">
<DockPanel Margin="{StaticResource Margin8}">
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock
Grid.Row="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSBCustomConfigDesc}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCustomConfigEnable}" />
<ToggleButton
x:Name="sbCustomConfigEnable"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
</StackPanel>
</Grid>
<WrapPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbAddProxyProtocolOutboundOnly}" />
<ToggleButton
x:Name="togAddProxyProtocolOutboundOnly4Singbox"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSetUpstreamProxyDetour}" />
<TextBox
x:Name="txtProxyDetour4Singbox"
Width="200"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefTextBox}" />
</StackPanel>
</WrapPanel>
<Grid Margin="{StaticResource Margin8}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBox
x:Name="sbCustomConfig"
Grid.Column="0"
VerticalAlignment="Stretch"
materialDesign:HintAssist.Hint="sing-box json config"
AcceptsReturn="True"
BorderThickness="1"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" />
<TextBox
x:Name="sbCustomTunConfig"
Grid.Column="2"
VerticalAlignment="Stretch"
materialDesign:HintAssist.Hint="sing-box json tun config"
AcceptsReturn="True"
BorderThickness="1"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
</Grid>
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</base:WindowBase>

View file

@ -0,0 +1,47 @@
using System.Reactive.Disposables;
using System.Windows;
using ReactiveUI;
namespace v2rayN.Views;
public partial class CustomConfigWindow
{
private static Config _config;
public CustomConfigWindow()
{
InitializeComponent();
this.Owner = Application.Current.MainWindow;
_config = AppHandler.Instance.Config;
ViewModel = new CustomConfigViewModel(UpdateViewHandler);
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.EnableCustomConfig4Ray, v => v.rayCustomConfigEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomConfig4Ray, v => v.rayCustomConfig.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AddProxyOnly4Ray, v => v.togAddProxyProtocolOutboundOnly4Ray.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ProxyDetour4Ray, v => v.txtProxyDetour4Ray.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCustomConfig4Singbox, v => v.sbCustomConfigEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomConfig4Singbox, v => v.sbCustomConfig.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomTunConfig4Singbox, v => v.sbCustomTunConfig.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AddProxyOnly4Singbox, v => v.togAddProxyProtocolOutboundOnly4Singbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ProxyDetour4Singbox, v => v.txtProxyDetour4Singbox.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
WindowsUtils.SetDarkBorder(this, AppHandler.Instance.Config.UiItem.CurrentTheme);
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
this.DialogResult = true;
break;
}
return await Task.FromResult(true);
}
}

View file

@ -173,6 +173,10 @@
x:Name="menuDNSSetting" x:Name="menuDNSSetting"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuDNSSetting}" /> Header="{x:Static resx:ResUI.menuDNSSetting}" />
<MenuItem
x:Name="menuCustomConfig"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuCustomConfig}" />
<MenuItem <MenuItem
x:Name="menuGlobalHotkeySetting" x:Name="menuGlobalHotkeySetting"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"

View file

@ -97,6 +97,7 @@ public partial class MainWindow
this.BindCommand(ViewModel, vm => vm.OptionSettingCmd, v => v.menuOptionSetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.OptionSettingCmd, v => v.menuOptionSetting).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RoutingSettingCmd, v => v.menuRoutingSetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RoutingSettingCmd, v => v.menuRoutingSetting).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.DNSSettingCmd, v => v.menuDNSSetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.DNSSettingCmd, v => v.menuDNSSetting).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CustomConfigCmd, v => v.menuCustomConfig).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GlobalHotkeySettingCmd, v => v.menuGlobalHotkeySetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GlobalHotkeySettingCmd, v => v.menuGlobalHotkeySetting).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RebootAsAdminCmd, v => v.menuRebootAsAdmin).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RebootAsAdminCmd, v => v.menuRebootAsAdmin).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ClearServerStatisticsCmd, v => v.menuClearServerStatistics).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ClearServerStatisticsCmd, v => v.menuClearServerStatistics).DisposeWith(disposables);
@ -186,6 +187,9 @@ public partial class MainWindow
case EViewAction.OptionSettingWindow: case EViewAction.OptionSettingWindow:
return (new OptionSettingWindow().ShowDialog() ?? false); return (new OptionSettingWindow().ShowDialog() ?? false);
case EViewAction.CustomConfigWindow:
return (new CustomConfigWindow().ShowDialog() ?? false);
case EViewAction.GlobalHotkeySettingWindow: case EViewAction.GlobalHotkeySettingWindow:
return (new GlobalHotkeySettingWindow().ShowDialog() ?? false); return (new GlobalHotkeySettingWindow().ShowDialog() ?? false);