Feat. custom config

DHR60 2025-07-12 23:29:21 +08:00
parent 4f560e86ab
commit c0cebf9cad
23 changed files with 995 additions and 4 deletions

View File

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

View File

@ -64,6 +64,7 @@ public sealed class AppHandler
SQLiteHelper.Instance.CreateTable<RoutingItem>();
SQLiteHelper.Instance.CreateTable<ProfileExItem>();
SQLiteHelper.Instance.CreateTable<DNSItem>();
SQLiteHelper.Instance.CreateTable<CustomConfigItem>();
return true;
}
@ -203,6 +204,16 @@ public sealed class AppHandler
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
#region Core Type

View File

@ -2220,6 +2220,54 @@ public class ConfigHandler
#endregion Simple 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
/// <summary>

View File

@ -0,0 +1,16 @@
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; }
}

View File

@ -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>
/// 查找类似 Please fill in the correct custom DNS 的本地化字符串。
/// </summary>
@ -852,6 +861,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Custom Config 的本地化字符串。
/// </summary>
public static string menuCustomConfig {
get {
return ResourceManager.GetString("menuCustomConfig", resourceCulture);
}
}
/// <summary>
/// 查找类似 DNS Settings 的本地化字符串。
/// </summary>
@ -2373,6 +2391,24 @@ 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>
/// 查找类似 Display GUI 的本地化字符串。
/// </summary>
@ -2706,6 +2742,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 v2ray Custom Config 的本地化字符串。
/// </summary>
public static string TbRayCustomConfig {
get {
return ResourceManager.GetString("TbRayCustomConfig", resourceCulture);
}
}
/// <summary>
/// 查找类似 Override Outbound Config, routing.balancers and routing.rules.outboundTag Only 的本地化字符串。
/// </summary>
public static string TbRayCustomConfigDesc {
get {
return ResourceManager.GetString("TbRayCustomConfigDesc", resourceCulture);
}
}
/// <summary>
/// 查找类似 Alias (remarks) 的本地化字符串。
/// </summary>
@ -2895,6 +2949,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Override Outbound And Endpoint Config Only 的本地化字符串。
/// </summary>
public static string TbSBCustomConfigDesc {
get {
return ResourceManager.GetString("TbSBCustomConfigDesc", resourceCulture);
}
}
/// <summary>
/// 查找类似 Encryption method (security) 的本地化字符串。
/// </summary>

View File

@ -1464,4 +1464,25 @@
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="FillCorrectConfigText" xml:space="preserve">
<value>Please fill in the correct custom config</value>
</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>Override Outbound Config, routing.balancers and routing.rules.outboundTag Only</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Override Outbound And Endpoint Config Only</value>
</data>
</root>

View File

@ -1464,4 +1464,25 @@
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="FillCorrectConfigText" xml:space="preserve">
<value>Please fill in the correct custom config</value>
</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>Override Outbound Config, routing.balancers and routing.rules.outboundTag Only</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Override Outbound And Endpoint Config Only</value>
</data>
</root>

View File

@ -1464,4 +1464,25 @@
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="FillCorrectConfigText" xml:space="preserve">
<value>Please fill in the correct custom config</value>
</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>Override Outbound Config, routing.balancers and routing.rules.outboundTag Only</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Override Outbound And Endpoint Config Only</value>
</data>
</root>

View File

@ -1464,4 +1464,25 @@
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="FillCorrectConfigText" xml:space="preserve">
<value>Please fill in the correct custom config</value>
</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>Override Outbound Config, routing.balancers and routing.rules.outboundTag Only</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Override Outbound And Endpoint Config Only</value>
</data>
</root>

View File

@ -1461,4 +1461,25 @@
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
<value>自定义 DNS 已启用,此页面配置将无效</value>
</data>
<data name="FillCorrectConfigText" xml:space="preserve">
<value>请填写正确的自定义配置</value>
</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>
</root>

View File

@ -1461,4 +1461,25 @@
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="FillCorrectConfigText" xml:space="preserve">
<value>Please fill in the correct custom config</value>
</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>Override Outbound Config, routing.balancers and routing.rules.outboundTag Only</value>
</data>
<data name="TbSBCustomConfigDesc" xml:space="preserve">
<value>Override Outbound And Endpoint Config Only</value>
</data>
</root>

View File

@ -4,6 +4,7 @@ using System.Net;
using System.Net.NetworkInformation;
using System.Reactive;
using System.Text;
using System.Text.Json.Nodes;
using DynamicData;
using ServiceLib.Models;
@ -83,7 +84,51 @@ public class CoreConfigSingboxService
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
var customConfig = await AppHandler.Instance.GetCustomConfigItem(ECoreType.sing_box);
if (customConfig.Enabled && (!customConfig.Config.IsNullOrEmpty()))
{
//var customConfigObj = JsonUtils.Deserialize<SingboxConfig>(customConfig.Config);
//if (customConfigObj != null)
//{
// customConfigObj.outbounds = JsonUtils.DeepCopy(singboxConfig.outbounds);
// singboxConfig = customConfigObj;
//}
// skip deserialize
var customConfigNode = JsonNode.Parse(customConfig.Config);
if (customConfigNode != null)
{
// customConfigNode["outbounds"] = JsonNode.Parse(JsonUtils.Serialize(singboxConfig.outbounds));
// append outbounds instead of override
if (customConfigNode["outbounds"] is JsonArray customOutboundsNode)
{
if (JsonNode.Parse(JsonUtils.Serialize(singboxConfig.outbounds)) is JsonArray newOutbounds)
{
foreach (var outbound in newOutbounds)
{
customOutboundsNode.Add(outbound?.DeepClone());
}
}
}
// endpoints
if (customConfigNode["endpoints"] is JsonArray customEndpointsNode)
{
if (singboxConfig.endpoints != null && JsonNode.Parse(JsonUtils.Serialize(singboxConfig.endpoints)) is JsonArray newEndpoints)
{
foreach (var endpoint in newEndpoints)
{
customEndpointsNode.Add(endpoint?.DeepClone());
}
}
}
ret.Data = customConfigNode.ToJsonString(new() { WriteIndented = true });
}
}
else
{
ret.Data = JsonUtils.Serialize(singboxConfig);
}
return ret;
}
catch (Exception ex)
@ -439,7 +484,50 @@ public class CoreConfigSingboxService
await ConvertGeo2Ruleset(singboxConfig);
ret.Success = true;
var customConfig = await AppHandler.Instance.GetCustomConfigItem(ECoreType.sing_box);
if (customConfig.Enabled && (!customConfig.Config.IsNullOrEmpty()))
{
//var customConfigObj = JsonUtils.Deserialize<SingboxConfig>(customConfig.Config);
//if (customConfigObj != null)
//{
// customConfigObj.outbounds = JsonUtils.DeepCopy(singboxConfig.outbounds);
// singboxConfig = customConfigObj;
//}
// skip deserialize
var customConfigNode = JsonNode.Parse(customConfig.Config);
if (customConfigNode != null)
{
// customConfigNode["outbounds"] = JsonNode.Parse(JsonUtils.Serialize(singboxConfig.outbounds));
// append outbounds instead of override
if (customConfigNode["outbounds"] is JsonArray customOutboundsNode)
{
if (JsonNode.Parse(JsonUtils.Serialize(singboxConfig.outbounds)) is JsonArray newOutbounds)
{
foreach (var outbound in newOutbounds)
{
customOutboundsNode.Add(outbound?.DeepClone());
}
}
}
// endpoints
if (customConfigNode["endpoints"] is JsonArray customEndpointsNode)
{
if (singboxConfig.endpoints != null && JsonNode.Parse(JsonUtils.Serialize(singboxConfig.endpoints)) is JsonArray newEndpoints)
{
foreach (var endpoint in newEndpoints)
{
customEndpointsNode.Add(endpoint?.DeepClone());
}
}
}
ret.Data = customConfigNode.ToJsonString(new() { WriteIndented = true });
}
}
else
{
ret.Data = JsonUtils.Serialize(singboxConfig);
}
return ret;
}
catch (Exception ex)

View File

@ -3,6 +3,7 @@ using System.Net.NetworkInformation;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ServiceLib.Models;
namespace ServiceLib.Services.CoreConfig;
@ -68,7 +69,39 @@ public class CoreConfigV2rayService
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
var customConfig = await AppHandler.Instance.GetCustomConfigItem(ECoreType.Xray);
if (customConfig.Enabled && (!customConfig.Config.IsNullOrEmpty()))
{
//var customConfigObj = JsonUtils.Deserialize<V2rayConfig>(customConfig.Config);
//if (customConfigObj != null)
//{
// customConfigObj.outbounds = JsonUtils.DeepCopy(v2rayConfig.outbounds);
// v2rayConfig = customConfigObj;
//}
// spik deserialize
var customConfigNode = JsonNode.Parse(customConfig.Config);
if (customConfigNode != null)
{
//customConfigNode["outbounds"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.outbounds));
// append outbounds instead of override
if (customConfigNode["outbounds"] is JsonArray customOutboundsNode)
{
if (JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.outbounds)) is JsonArray newOutbounds)
{
foreach (var outbound in newOutbounds)
{
customOutboundsNode.Add(outbound?.DeepClone());
}
}
}
ret.Data = customConfigNode.ToJsonString(new() { WriteIndented = true });
}
}
else
{
ret.Data = JsonUtils.Serialize(v2rayConfig);
}
return ret;
}
catch (Exception ex)
@ -197,7 +230,82 @@ public class CoreConfigV2rayService
}
ret.Success = true;
var customConfig = await AppHandler.Instance.GetCustomConfigItem(ECoreType.Xray);
if (customConfig.Enabled && (!customConfig.Config.IsNullOrEmpty()))
{
//var customConfigObj = JsonUtils.Deserialize<V2rayConfig>(customConfig.Config);
//if (customConfigObj != null)
//{
// foreach (var rule in customConfigObj.routing.rules)
// {
// if (rule.outboundTag == Global.ProxyTag)
// {
// rule.outboundTag = null;
// rule.balancerTag = balancer.tag;
// }
// }
// customConfigObj.routing.balancers = JsonUtils.DeepCopy(v2rayConfig.routing.balancers);
// customConfigObj.outbounds = JsonUtils.DeepCopy(v2rayConfig.outbounds);
// v2rayConfig = customConfigObj;
//}
// skip deserialize
var customConfigNode = JsonNode.Parse(customConfig.Config);
if (customConfigNode != null)
{
var rulesNode = customConfigNode["routing"]?["rules"];
if (rulesNode != null)
{
foreach (var rule in rulesNode.AsArray())
{
if (rule["outboundTag"]?.GetValue<string>() == Global.ProxyTag)
{
rule["outboundTag"] = null;
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));
}
// append outbounds instead of override
if (customConfigNode["outbounds"] is JsonArray customOutboundsNode)
{
if (JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.outbounds)) is JsonArray newOutbounds)
{
foreach (var outbound in newOutbounds)
{
customOutboundsNode.Add(outbound?.DeepClone());
}
}
}
ret.Data = customConfigNode.ToJsonString(new() { WriteIndented = true });
}
}
else
{
ret.Data = JsonUtils.Serialize(v2rayConfig);
}
return ret;
}
catch (Exception ex)

View File

@ -0,0 +1,106 @@
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; }
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;
var item2 = await AppHandler.Instance.GetCustomConfigItem(ECoreType.sing_box);
EnableCustomConfig4Singbox = item2?.Enabled ?? false;
CustomConfig4Singbox = item2?.Config ?? string.Empty;
CustomTunConfig4Singbox = item2?.TunConfig ?? 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;
if (CustomConfig4Ray.IsNotEmpty())
{
item.Config = CustomConfig4Ray;
}
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;
var hasChanges = false;
if (CustomConfig4Singbox.IsNotEmpty())
{
item.Config = CustomConfig4Singbox;
hasChanges = true;
}
if (CustomTunConfig4Singbox.IsNotEmpty())
{
item.TunConfig = CustomTunConfig4Singbox;
hasChanges = true;
}
if (hasChanges)
{
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> DNSSettingCmd { get; }
public ReactiveCommand<Unit, Unit> CustomConfigCmd { get; }
public ReactiveCommand<Unit, Unit> GlobalHotkeySettingCmd { get; }
public ReactiveCommand<Unit, Unit> RebootAsAdminCmd { get; }
public ReactiveCommand<Unit, Unit> ClearServerStatisticsCmd { get; }
@ -169,6 +170,10 @@ public class MainWindowViewModel : MyReactiveObject
{
await DNSSettingAsync();
});
CustomConfigCmd = ReactiveCommand.CreateFromTask(async () =>
{
await CustomConfigAsync();
});
GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () =>
{
if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true)
@ -220,6 +225,7 @@ public class MainWindowViewModel : MyReactiveObject
await ConfigHandler.InitBuiltinRouting(_config);
await ConfigHandler.InitBuiltinDNS(_config);
await ConfigHandler.InitBuiltinCustomConfig(_config);
await ProfileExHandler.Instance.Init();
await CoreHandler.Instance.Init(_config, UpdateHandler);
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()
{
ProcUtils.RebootAsAdmin();

View File

@ -0,0 +1,144 @@
<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}">
<Grid Margin="{StaticResource Margin4}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRayCustomConfigDesc}" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCustomConfigEnable}" />
<ToggleSwitch
x:Name="rayCustomConfigEnable"
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
</Grid>
<TextBox
x:Name="rayCustomConfig"
Grid.Row="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderThickness="1"
Classes="TextArea"
Margin="{StaticResource Margin4}"
TextWrapping="Wrap"
Watermark="xray json config" />
</Grid>
</TabItem>
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.TbCustomConfigSingbox}">
<Grid Margin="{StaticResource Margin4}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBCustomConfigDesc}" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCustomConfigEnable}" />
<ToggleSwitch
x:Name="sbCustomConfigEnable"
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
</Grid>
<Grid Grid.Row="2" Margin="{StaticResource Margin4}" ColumnDefinitions="*,10,*">
<TextBox
x:Name="sbCustomConfig"
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderThickness="1"
Classes="TextArea"
Margin="{StaticResource Margin4}"
TextWrapping="Wrap"
Watermark="sing-box json config" />
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" />
<TextBox
x:Name="sbCustomTunConfig"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderThickness="1"
Classes="TextArea"
Margin="{StaticResource Margin4}"
TextWrapping="Wrap"
Watermark="sing-box json tun config" />
</Grid>
</Grid>
</TabItem>
</TabControl>
</DockPanel>
</Window>

View File

@ -0,0 +1,42 @@
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.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.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="menuRoutingSetting" Header="{x:Static resx:ResUI.menuRoutingSetting}" />
<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}" />
<Separator />
<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.RoutingSettingCmd, v => v.menuRoutingSetting).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.RebootAsAdminCmd, v => v.menuRebootAsAdmin).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:
return await new DNSSettingWindow().ShowDialog<bool>(this);
case EViewAction.CustomConfigWindow:
return await new CustomConfigWindow().ShowDialog<bool>(this);
case EViewAction.RoutingSettingWindow:
return await new RoutingSettingWindow().ShowDialog<bool>(this);

View File

@ -0,0 +1,167 @@
<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}">
<Grid Margin="{StaticResource Margin8}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbRayCustomConfigDesc}" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCustomConfigEnable}" />
<ToggleButton
x:Name="rayCustomConfigEnable"
Grid.Column="1"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
</Grid>
<TextBox
x:Name="rayCustomConfig"
Grid.Row="2"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
materialDesign:HintAssist.Hint="xray json config"
AcceptsReturn="True"
BorderThickness="1"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
</Grid>
</TabItem>
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.TbCustomConfigSingbox}">
<Grid Margin="{StaticResource Margin8}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSBCustomConfigDesc}" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCustomConfigEnable}" />
<ToggleButton
x:Name="sbCustomConfigEnable"
Grid.Column="1"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
</Grid>
<Grid Grid.Row="2" Margin="{StaticResource Margin8}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBox
x:Name="sbCustomConfig"
Grid.Column="0"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Stretch"
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"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
materialDesign:HintAssist.Hint="sing-box json tun config"
AcceptsReturn="True"
BorderThickness="1"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
</Grid>
</Grid>
</TabItem>
</TabControl>
</DockPanel>
</base:WindowBase>

View File

@ -0,0 +1,43 @@
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.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.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"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuDNSSetting}" />
<MenuItem
x:Name="menuCustomConfig"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuCustomConfig}" />
<MenuItem
x:Name="menuGlobalHotkeySetting"
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.RoutingSettingCmd, v => v.menuRoutingSetting).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.RebootAsAdminCmd, v => v.menuRebootAsAdmin).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ClearServerStatisticsCmd, v => v.menuClearServerStatistics).DisposeWith(disposables);
@ -186,6 +187,9 @@ public partial class MainWindow
case EViewAction.OptionSettingWindow:
return (new OptionSettingWindow().ShowDialog() ?? false);
case EViewAction.CustomConfigWindow:
return (new CustomConfigWindow().ShowDialog() ?? false);
case EViewAction.GlobalHotkeySettingWindow:
return (new GlobalHotkeySettingWindow().ShowDialog() ?? false);