From 984b36d34e71c5efc225505eac23c84b62f851dc Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 15 Jun 2025 12:33:02 +0800
Subject: [PATCH 01/50] Update Directory.Packages.props
---
v2rayN/Directory.Packages.props | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props
index e9c8d492..47fdcdd9 100644
--- a/v2rayN/Directory.Packages.props
+++ b/v2rayN/Directory.Packages.props
@@ -5,21 +5,21 @@
false
-
-
-
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
From d239c627f3d462764c395ba3c0c793338ff00783 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 15 Jun 2025 12:33:21 +0800
Subject: [PATCH 02/50] Use File.SetUnixFileMode to set the execute permission
first. If that fails, use chmod to set the execute permission.
https://github.com/2dust/v2rayN/issues/7416
---
v2rayN/ServiceLib/Common/Utils.cs | 36 +++++++++++++++++++++++++++++--
1 file changed, 34 insertions(+), 2 deletions(-)
diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs
index 03a1a860..f7447dfb 100644
--- a/v2rayN/ServiceLib/Common/Utils.cs
+++ b/v2rayN/ServiceLib/Common/Utils.cs
@@ -582,7 +582,7 @@ public class Utils
var result = await cmd.ExecuteBufferedAsync();
if (result.IsSuccess)
{
- return result.StandardOutput.ToString();
+ return result.StandardOutput ?? "";
}
Logging.SaveLog(result.ToString() ?? "");
@@ -839,14 +839,46 @@ public class Utils
public static async Task SetLinuxChmod(string? fileName)
{
if (fileName.IsNullOrEmpty())
+ {
return null;
+ }
+ if (SetUnixFileMode(fileName))
+ {
+ Logging.SaveLog($"Successfully set the file execution permission, {fileName}");
+ return "";
+ }
+
if (fileName.Contains(' '))
+ {
fileName = fileName.AppendQuotes();
- //File.SetUnixFileMode(fileName, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
+ }
var arg = new List() { "-c", $"chmod +x {fileName}" };
return await GetCliWrapOutput(Global.LinuxBash, arg);
}
+ public static bool SetUnixFileMode(string? fileName)
+ {
+ try
+ {
+ if (fileName.IsNullOrEmpty())
+ {
+ return false;
+ }
+
+ if (File.Exists(fileName))
+ {
+ var currentMode = File.GetUnixFileMode(fileName);
+ File.SetUnixFileMode(fileName, currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog("SetUnixFileMode", ex);
+ }
+ return false;
+ }
+
public static async Task GetLinuxFontFamily(string lang)
{
// var arg = new List() { "-c", $"fc-list :lang={lang} family" };
From 9e2336a71e90fcaeca3bb6390ff634b3f071213e Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 15 Jun 2025 14:16:30 +0800
Subject: [PATCH 03/50] The notification pop-up position is changed to the top
right
https://github.com/2dust/v2rayN/issues/7421
---
v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
index e368d297..6aa89db6 100644
--- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
@@ -29,7 +29,7 @@ public partial class MainWindow : ReactiveWindow
InitializeComponent();
_config = AppHandler.Instance.Config;
- _manager = new WindowNotificationManager(TopLevel.GetTopLevel(this)) { MaxItems = 3, Position = NotificationPosition.BottomRight };
+ _manager = new WindowNotificationManager(TopLevel.GetTopLevel(this)) { MaxItems = 3, Position = NotificationPosition.TopRight };
this.KeyDown += MainWindow_KeyDown;
menuSettingsSetUWP.Click += menuSettingsSetUWP_Click;
From 7065dabc94cf1c551027d120e52694ebc55fb1c6 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 15 Jun 2025 14:35:59 +0800
Subject: [PATCH 04/50] If it is not in China area, no update is required
https://github.com/2dust/v2rayN/issues/7417
---
v2rayN/ServiceLib/Services/UpdateService.cs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs
index 636f3616..c4acbe80 100644
--- a/v2rayN/ServiceLib/Services/UpdateService.cs
+++ b/v2rayN/ServiceLib/Services/UpdateService.cs
@@ -485,6 +485,12 @@ public class UpdateService
private async Task UpdateOtherFiles(Config config, Action updateFunc)
{
+ //If it is not in China area, no update is required
+ if (config.ConstItem.GeoSourceUrl.IsNotEmpty())
+ {
+ return;
+ }
+
_updateFunc = updateFunc;
foreach (var url in Global.OtherGeoUrls)
From 2056377f5599b13bc0e07c923c6dd073095333a6 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 15 Jun 2025 14:39:02 +0800
Subject: [PATCH 05/50] up 7.12.6
---
v2rayN/Directory.Build.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props
index 78438651..837a276d 100644
--- a/v2rayN/Directory.Build.props
+++ b/v2rayN/Directory.Build.props
@@ -1,7 +1,7 @@
- 7.12.5
+ 7.12.6
From 9ddf0b42e7cc62990f0a65441f596dc5267601ac Mon Sep 17 00:00:00 2001
From: DHR60
Date: Sun, 15 Jun 2025 14:41:47 +0800
Subject: [PATCH 06/50] Fix Observatory (#7437)
* Enables concurrency for observatory
* Adds burst observatory for least load balancer
---
.../CoreConfig/CoreConfigV2rayService.cs | 20 +++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
index b1dc1ffd..42057c09 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
@@ -1366,16 +1366,32 @@ public class CoreConfigV2rayService
private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad)
{
- if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.LeastPing)
+ if (multipleLoad == EMultipleLoad.LeastPing)
{
var observatory = new Observatory4Ray
{
subjectSelector = [Global.ProxyTag],
probeUrl = AppHandler.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
- probeInterval = "3m"
+ probeInterval = "3m",
+ enableConcurrency = true,
};
v2rayConfig.observatory = observatory;
}
+ else if (multipleLoad == EMultipleLoad.LeastLoad)
+ {
+ var burstObservatory = new BurstObservatory4Ray
+ {
+ subjectSelector = [Global.ProxyTag],
+ pingConfig = new()
+ {
+ destination = AppHandler.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
+ interval = "5m",
+ timeout = "30s",
+ sampling = 2,
+ }
+ };
+ v2rayConfig.burstObservatory = burstObservatory;
+ }
var strategyType = multipleLoad switch
{
EMultipleLoad.Random => "random",
From 06500e0218f524a1653b25f80f64bbc99c8f5f6a Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 15 Jun 2025 17:33:11 +0800
Subject: [PATCH 07/50] When testing, start the core and then delay 1s before
starting the test
https://github.com/2dust/v2rayN/issues/7391
---
v2rayN/ServiceLib/Services/SpeedtestService.cs | 13 +++++++------
v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs | 1 +
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs
index f4cd64a2..998cedcc 100644
--- a/v2rayN/ServiceLib/Services/SpeedtestService.cs
+++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs
@@ -196,6 +196,7 @@ public class SpeedtestService
{
return false;
}
+ await Task.Delay(1000);
var downloadHandle = new DownloadService();
@@ -255,9 +256,13 @@ public class SpeedtestService
try
{
pid = await CoreHandler.Instance.LoadCoreConfigSpeedtest(it);
- if (pid > 0)
+ if (pid < 0)
{
- await Task.Delay(500);
+ UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
+ }
+ else
+ {
+ await Task.Delay(1000);
var delay = await DoRealPing(downloadHandle, it);
if (blSpeedTest)
{
@@ -271,10 +276,6 @@ public class SpeedtestService
}
}
}
- else
- {
- UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
- }
}
catch (Exception ex)
{
diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs
index 5edab5d4..7bae19be 100644
--- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs
@@ -552,6 +552,7 @@ public class MainWindowViewModel : MyReactiveObject
{
await LoadCore();
await SysProxyHandler.UpdateSysProxy(_config, false);
+ await Task.Delay(1000);
});
Locator.Current.GetService()?.TestServerAvailability();
From cb6122f87299a96f2f57575ef9d37d13cd7c28e9 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Mon, 16 Jun 2025 10:48:35 +0800
Subject: [PATCH 08/50] First scroll horizontally to the initial position to
avoid the control crash bug
https://github.com/2dust/v2rayN/issues/7387
---
.../Views/ProfilesView.axaml.cs | 20 ++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs
index 9bcdb85a..f72c1f51 100644
--- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs
@@ -215,7 +215,7 @@ public partial class ProfilesView : ReactiveUserControl
await ViewModel.RefreshServersBiz();
}
- if (lstProfiles.SelectedIndex > 0)
+ if (lstProfiles.SelectedIndex >= 0)
{
lstProfiles.ScrollIntoView(lstProfiles.SelectedItem, null);
}
@@ -347,6 +347,24 @@ public partial class ProfilesView : ReactiveUserControl
{
try
{
+ //First scroll horizontally to the initial position to avoid the control crash bug
+ if (lstProfiles.SelectedIndex >= 0)
+ {
+ lstProfiles.ScrollIntoView(lstProfiles.SelectedItem, lstProfiles.Columns[0]);
+ }
+ else
+ {
+ var model = lstProfiles.ItemsSource.Cast();
+ if (model.Any())
+ {
+ lstProfiles.ScrollIntoView(model.First(), lstProfiles.Columns[0]);
+ }
+ else
+ {
+ return;
+ }
+ }
+
foreach (var it in lstProfiles.Columns)
{
it.Width = new DataGridLength(1, DataGridLengthUnitType.Auto);
From 0e5ac65f55f0d6ec04f5df1ff4e28eb12b4ebfe6 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Mon, 16 Jun 2025 15:06:18 +0800
Subject: [PATCH 09/50] Improved network speed display when using clash api
without proxy and direct
---
v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs
index 82b48aff..07a2ec70 100644
--- a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs
@@ -500,8 +500,16 @@ public class StatusBarViewModel : MyReactiveObject
{
try
{
- SpeedProxyDisplay = string.Format(ResUI.SpeedDisplayText, Global.ProxyTag, Utils.HumanFy(update.ProxyUp), Utils.HumanFy(update.ProxyDown));
- SpeedDirectDisplay = string.Format(ResUI.SpeedDisplayText, Global.DirectTag, Utils.HumanFy(update.DirectUp), Utils.HumanFy(update.DirectDown));
+ if (_config.IsRunningCore(ECoreType.sing_box))
+ {
+ SpeedProxyDisplay = string.Format(ResUI.SpeedDisplayText, EInboundProtocol.mixed, Utils.HumanFy(update.ProxyUp), Utils.HumanFy(update.ProxyDown));
+ SpeedDirectDisplay = string.Empty;
+ }
+ else
+ {
+ SpeedProxyDisplay = string.Format(ResUI.SpeedDisplayText, Global.ProxyTag, Utils.HumanFy(update.ProxyUp), Utils.HumanFy(update.ProxyDown));
+ SpeedDirectDisplay = string.Format(ResUI.SpeedDisplayText, Global.DirectTag, Utils.HumanFy(update.DirectUp), Utils.HumanFy(update.DirectDown));
+ }
}
catch
{
From 298bb64e667d606390290e9e343d968364fefd19 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Thu, 19 Jun 2025 10:04:48 +0800
Subject: [PATCH 10/50] Routing rules do not determine whether it is version V3
---
v2rayN/ServiceLib/Handler/ConfigHandler.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
index 6a25b56f..66579b92 100644
--- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs
+++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
@@ -1979,7 +1979,7 @@ public class ConfigHandler
items = await AppHandler.Instance.RoutingItems();
}
- if (!blImportAdvancedRules && items.Where(t => t.Remarks.StartsWith(ver)).ToList().Count > 0)
+ if (!blImportAdvancedRules && items.Count > 0)
{
return 0;
}
From 93a20852f50e939f3427da3e410d90be8762c391 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Thu, 19 Jun 2025 10:48:47 +0800
Subject: [PATCH 11/50] Optimize the UI for routing settings
---
.../Views/RoutingSettingWindow.axaml | 177 +++++++----
v2rayN/v2rayN/Views/RoutingSettingWindow.xaml | 300 ++++++++++--------
2 files changed, 267 insertions(+), 210 deletions(-)
diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml
index 59b34a75..698b2346 100644
--- a/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml
@@ -24,28 +24,11 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml b/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml
index 97eb5754..671c226f 100644
--- a/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml
+++ b/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -122,82 +79,145 @@
Style="{StaticResource DefButton}" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
From e46f68065105950cfddbb4acd2d0017a129f35b1 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Thu, 19 Jun 2025 11:24:33 +0800
Subject: [PATCH 12/50] Optimize the UI for dns settings
---
.../Views/DNSSettingWindow.axaml | 57 ++++++++++---------
1 file changed, 31 insertions(+), 26 deletions(-)
diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml
index 96c72c47..efdf4eda 100644
--- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml
@@ -34,7 +34,7 @@
IsCancel="True" />
-
+
@@ -90,16 +90,18 @@
-
+
-
+ MinLines="10"
+ TextWrapping="Wrap" />
+
@@ -144,31 +146,34 @@
-
+ Header="HTTP/SOCKS">
+
+
-
+ Header="{x:Static resx:ResUI.TbSettingsTunMode}">
+
+
+
From a46a4ad7c1219f5f5d558c7c1c738cd287a697ea Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Thu, 19 Jun 2025 11:59:20 +0800
Subject: [PATCH 13/50] up 7.12.7
---
v2rayN/Directory.Build.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props
index 837a276d..4e1f4406 100644
--- a/v2rayN/Directory.Build.props
+++ b/v2rayN/Directory.Build.props
@@ -1,7 +1,7 @@
- 7.12.6
+ 7.12.7
From fefa7ded5ad37d73d98057f03be20f828fa43244 Mon Sep 17 00:00:00 2001
From: DHR60
Date: Sat, 21 Jun 2025 16:45:17 +0800
Subject: [PATCH 14/50] Optimize proxy chain handling for multiple nodes
(#7468)
* Improves outbound proxy chain handling.
* Improves sing-box outbound proxy chain handling.
* AI-optimized code
---
.../CoreConfig/CoreConfigSingboxService.cs | 205 +++++++++++++++---
.../CoreConfig/CoreConfigV2rayService.cs | 193 ++++++++++++++++-
2 files changed, 363 insertions(+), 35 deletions(-)
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs
index 27ed39b9..2f3d4356 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs
@@ -336,7 +336,7 @@ public class CoreConfigSingboxService
await GenExperimental(singboxConfig);
singboxConfig.outbounds.RemoveAt(0);
- var tagProxy = new List();
+ var proxyProfiles = new List();
foreach (var it in selecteds)
{
if (it.ConfigType == EConfigType.Custom)
@@ -370,42 +370,18 @@ public class CoreConfigSingboxService
}
//outbound
- var outbound = JsonUtils.Deserialize(txtOutbound);
- await GenOutbound(item, outbound);
- outbound.tag = $"{Global.ProxyTag}-{tagProxy.Count + 1}";
- singboxConfig.outbounds.Insert(0, outbound);
- tagProxy.Add(outbound.tag);
+ proxyProfiles.Add(item);
}
- if (tagProxy.Count <= 0)
+ if (proxyProfiles.Count <= 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
+ await GenOutboundsList(proxyProfiles, singboxConfig);
await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
- //add urltest outbound
- var outUrltest = new Outbound4Sbox
- {
- type = "urltest",
- tag = $"{Global.ProxyTag}-auto",
- outbounds = tagProxy,
- interrupt_exist_connections = false,
- };
- singboxConfig.outbounds.Insert(0, outUrltest);
-
- //add selector outbound
- var outSelector = new Outbound4Sbox
- {
- type = "selector",
- tag = Global.ProxyTag,
- outbounds = JsonUtils.DeepCopy(tagProxy),
- interrupt_exist_connections = false,
- };
- outSelector.outbounds.Insert(0, outUrltest.tag);
- singboxConfig.outbounds.Insert(0, outSelector);
-
ret.Success = true;
ret.Data = JsonUtils.Serialize(singboxConfig);
return ret;
@@ -974,6 +950,179 @@ public class CoreConfigSingboxService
return 0;
}
+ private async Task GenOutboundsList(List nodes, SingboxConfig singboxConfig)
+ {
+ try
+ {
+ // Get outbound template and initialize lists
+ var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
+ if (txtOutbound.IsNullOrEmpty())
+ {
+ return 0;
+ }
+
+ var resultOutbounds = new List();
+ var prevOutbounds = new List(); // Separate list for prev outbounds
+ var proxyTags = new List(); // For selector and urltest outbounds
+
+ // Cache for chain proxies to avoid duplicate generation
+ var chainProxyCache = new Dictionary();
+ var prevProxyTags = new Dictionary(); // Map from profile name to tag
+ int prevIndex = 0; // Index for prev outbounds
+
+ // Process each node
+ int index = 0;
+ foreach (var node in nodes)
+ {
+ index++;
+
+ // Skip unsupported config types
+ if (node.ConfigType is EConfigType.Custom)
+ {
+ continue;
+ }
+
+ // Handle proxy chain
+ string? prevTag = null;
+ Outbound4Sbox? nextOutbound = null;
+
+ if (node.Subid.IsNotEmpty())
+ {
+ // Check if chain proxy is already cached
+ if (chainProxyCache.TryGetValue(node.Subid, out var chainProxy))
+ {
+ prevTag = chainProxy.Item1;
+ nextOutbound = chainProxy.Item2;
+ }
+ else
+ {
+ // Generate chain proxy and cache it
+ var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
+ if (subItem != null)
+ {
+ // Process previous proxy
+ if (!subItem.PrevProfile.IsNullOrEmpty())
+ {
+ // Check if this previous proxy was already created
+ if (prevProxyTags.TryGetValue(subItem.PrevProfile, out var existingTag))
+ {
+ prevTag = existingTag;
+ }
+ else
+ {
+ var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
+ if (prevNode != null && prevNode.ConfigType != EConfigType.Custom)
+ {
+ prevIndex++;
+ var prevOutbound = JsonUtils.Deserialize(txtOutbound);
+ await GenOutbound(prevNode, prevOutbound);
+
+ prevTag = $"{Global.ProxyTag}-prev-{prevIndex}";
+ prevOutbound.tag = prevTag;
+ prevProxyTags[subItem.PrevProfile] = prevTag;
+
+ // Add to prev outbounds list (will be added at the end)
+ prevOutbounds.Add(prevOutbound);
+ }
+ }
+ }
+
+ // Process next proxy
+ var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
+ if (nextNode != null && nextNode.ConfigType != EConfigType.Custom)
+ {
+ nextOutbound = JsonUtils.Deserialize(txtOutbound);
+ await GenOutbound(nextNode, nextOutbound);
+ }
+
+ // Cache the chain proxy
+ chainProxyCache[node.Subid] = (prevTag, nextOutbound);
+ }
+ }
+ }
+
+ // Create main outbound
+ var outbound = JsonUtils.Deserialize(txtOutbound);
+
+ await GenOutbound(node, outbound);
+ outbound.tag = $"{Global.ProxyTag}-{index}";
+
+ // Configure proxy chain relationships
+ if (nextOutbound != null)
+ {
+ // If there's a next proxy, it should be the final outbound in the chain
+ var originalTag = outbound.tag;
+ outbound.tag = $"mid-{Global.ProxyTag}-{index}";
+
+ var nextOutboundCopy = JsonUtils.DeepCopy(nextOutbound);
+ nextOutboundCopy.tag = originalTag;
+ nextOutboundCopy.detour = outbound.tag; // Use detour instead of sockopt
+
+ if (prevTag != null)
+ {
+ outbound.detour = prevTag;
+ }
+
+ // Add to proxy tags for selector/urltest
+ proxyTags.Add(originalTag);
+
+ // Add in reverse order to ensure final outbound is added first
+ resultOutbounds.Add(nextOutboundCopy); // Final outbound (exposed to internet)
+ resultOutbounds.Add(outbound); // Middle outbound
+ }
+ else
+ {
+ // If no next proxy, the main outbound is the final one
+ if (prevTag != null)
+ {
+ outbound.detour = prevTag;
+ }
+
+ // Add to proxy tags for selector/urltest
+ proxyTags.Add(outbound.tag);
+ resultOutbounds.Add(outbound);
+ }
+ }
+
+ // Add urltest outbound (auto selection based on latency)
+ if (proxyTags.Count > 0)
+ {
+ var outUrltest = new Outbound4Sbox
+ {
+ type = "urltest",
+ tag = $"{Global.ProxyTag}-auto",
+ outbounds = proxyTags,
+ interrupt_exist_connections = false,
+ };
+
+ // Add selector outbound (manual selection)
+ var outSelector = new Outbound4Sbox
+ {
+ type = "selector",
+ tag = Global.ProxyTag,
+ outbounds = JsonUtils.DeepCopy(proxyTags),
+ interrupt_exist_connections = false,
+ };
+ outSelector.outbounds.Insert(0, outUrltest.tag);
+
+ // Insert these at the beginning
+ resultOutbounds.Insert(0, outUrltest);
+ resultOutbounds.Insert(0, outSelector);
+ }
+
+ // Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds
+ resultOutbounds.AddRange(prevOutbounds);
+ resultOutbounds.AddRange(singboxConfig.outbounds);
+ singboxConfig.outbounds = resultOutbounds;
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(_tag, ex);
+ }
+
+ return 0;
+ }
+
private async Task GenRouting(SingboxConfig singboxConfig)
{
try
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
index 42057c09..e1d103f2 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
@@ -113,7 +113,7 @@ public class CoreConfigV2rayService
await GenStatistic(v2rayConfig);
v2rayConfig.outbounds.RemoveAt(0);
- var tagProxy = new List();
+ var proxyProfiles = new List();
foreach (var it in selecteds)
{
if (it.ConfigType == EConfigType.Custom)
@@ -151,17 +151,14 @@ public class CoreConfigV2rayService
}
//outbound
- var outbound = JsonUtils.Deserialize(txtOutbound);
- await GenOutbound(item, outbound);
- outbound.tag = $"{Global.ProxyTag}-{tagProxy.Count + 1}";
- v2rayConfig.outbounds.Insert(0, outbound);
- tagProxy.Add(outbound.tag);
+ proxyProfiles.Add(item);
}
- if (tagProxy.Count <= 0)
+ if (proxyProfiles.Count <= 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
+ await GenOutboundsList(proxyProfiles, v2rayConfig);
//add balancers
await GenBalancer(v2rayConfig, multipleLoad);
@@ -1364,6 +1361,188 @@ public class CoreConfigV2rayService
return 0;
}
+ private async Task GenOutboundsList(List nodes, V2rayConfig v2rayConfig)
+ {
+ try
+ {
+ // Get template and initialize list
+ var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
+ if (txtOutbound.IsNullOrEmpty())
+ {
+ return 0;
+ }
+
+ var resultOutbounds = new List();
+ var prevOutbounds = new List(); // Separate list for prev outbounds and fragment
+
+ // Handle fragment outbound
+ Outbounds4Ray? fragmentOutbound = null;
+ if (_config.CoreBasicItem.EnableFragment)
+ {
+ fragmentOutbound = new Outbounds4Ray
+ {
+ protocol = "freedom",
+ tag = $"fragment-{Global.ProxyTag}",
+ settings = new()
+ {
+ fragment = new()
+ {
+ packets = _config.Fragment4RayItem?.Packets,
+ length = _config.Fragment4RayItem?.Length,
+ interval = _config.Fragment4RayItem?.Interval
+ }
+ }
+ };
+ // Add to prevOutbounds instead of v2rayConfig.outbounds
+ prevOutbounds.Add(fragmentOutbound);
+ }
+
+ // Cache for chain proxies to avoid duplicate generation
+ var chainProxyCache = new Dictionary();
+ var prevProxyTags = new Dictionary(); // Map from profile name to tag
+ int prevIndex = 0; // Index for prev outbounds
+
+ // Process nodes
+ int index = 0;
+ foreach (var node in nodes)
+ {
+ index++;
+
+ // Skip unsupported config types
+ if (node.ConfigType is EConfigType.Custom or EConfigType.Hysteria2 or EConfigType.TUIC)
+ {
+ continue;
+ }
+
+ // Handle proxy chain
+ string? prevTag = null;
+ Outbounds4Ray? nextOutbound = null;
+
+ if (!node.Subid.IsNullOrEmpty())
+ {
+ // Check if chain proxy is already cached
+ if (chainProxyCache.TryGetValue(node.Subid, out var chainProxy))
+ {
+ prevTag = chainProxy.Item1;
+ nextOutbound = chainProxy.Item2;
+ }
+ else
+ {
+ // Generate chain proxy and cache it
+ var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
+ if (subItem != null)
+ {
+ // Process previous proxy
+ if (!subItem.PrevProfile.IsNullOrEmpty())
+ {
+ // Check if this previous proxy was already created
+ if (prevProxyTags.TryGetValue(subItem.PrevProfile, out var existingTag))
+ {
+ prevTag = existingTag;
+ }
+ else
+ {
+ var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
+ if (prevNode != null
+ && prevNode.ConfigType != EConfigType.Custom
+ && prevNode.ConfigType != EConfigType.Hysteria2
+ && prevNode.ConfigType != EConfigType.TUIC)
+ {
+ prevIndex++;
+ var prevOutbound = JsonUtils.Deserialize(txtOutbound);
+ await GenOutbound(prevNode, prevOutbound);
+
+ prevTag = $"{Global.ProxyTag}-prev-{prevIndex}";
+ prevOutbound.tag = prevTag;
+ prevProxyTags[subItem.PrevProfile] = prevTag;
+
+ // Set fragment if needed
+ if (fragmentOutbound != null && prevOutbound.streamSettings?.security.IsNullOrEmpty() == false)
+ {
+ prevOutbound.streamSettings.sockopt = new()
+ {
+ dialerProxy = fragmentOutbound.tag
+ };
+ }
+
+ // Add to prev outbounds list (will be added at the end)
+ prevOutbounds.Add(prevOutbound);
+ }
+ }
+ }
+
+ // Process next proxy
+ var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
+ if (nextNode != null
+ && nextNode.ConfigType != EConfigType.Custom
+ && nextNode.ConfigType != EConfigType.Hysteria2
+ && nextNode.ConfigType != EConfigType.TUIC)
+ {
+ nextOutbound = JsonUtils.Deserialize(txtOutbound);
+ await GenOutbound(nextNode, nextOutbound);
+ }
+
+ // Cache the chain proxy
+ chainProxyCache[node.Subid] = (prevTag, nextOutbound);
+ }
+ }
+ }
+
+ // Create main outbound
+ var outbound = JsonUtils.Deserialize(txtOutbound);
+
+ await GenOutbound(node, outbound);
+ outbound.tag = $"{Global.ProxyTag}-{index}";
+
+ // Configure proxy chain relationships
+ if (nextOutbound != null)
+ {
+ // If there's a next proxy, it should be the final outbound in the chain
+ var originalTag = outbound.tag;
+ outbound.tag = $"mid-{Global.ProxyTag}-{index}";
+
+ var nextOutboundCopy = JsonUtils.DeepCopy(nextOutbound);
+ nextOutboundCopy.tag = originalTag;
+ nextOutboundCopy.streamSettings.sockopt = new() { dialerProxy = outbound.tag };
+
+ if (prevTag != null)
+ {
+ outbound.streamSettings.sockopt = new() { dialerProxy = prevTag };
+ }
+
+ // Add in reverse order to ensure final outbound is added first
+ resultOutbounds.Add(nextOutboundCopy); // Final outbound (exposed to internet)
+ resultOutbounds.Add(outbound); // Middle outbound
+ }
+ else
+ {
+ // If no next proxy, the main outbound is the final one
+ if (prevTag != null)
+ {
+ outbound.streamSettings.sockopt = new() { dialerProxy = prevTag };
+ }
+ else if (fragmentOutbound != null && outbound.streamSettings?.security.IsNullOrEmpty() == false)
+ {
+ outbound.streamSettings.sockopt = new() { dialerProxy = fragmentOutbound.tag };
+ }
+
+ resultOutbounds.Add(outbound);
+ }
+ }
+
+ // Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
+ resultOutbounds.AddRange(prevOutbounds);
+ resultOutbounds.AddRange(v2rayConfig.outbounds);
+ v2rayConfig.outbounds = resultOutbounds;
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(_tag, ex);
+ }
+
+ return 0;
+ }
+
private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad)
{
if (multipleLoad == EMultipleLoad.LeastPing)
From f947f63e6df395b4e2f9ccc6f8712dbe60d34bb7 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sat, 21 Jun 2025 16:54:36 +0800
Subject: [PATCH 15/50] Display or hide the main window menu in the tray and
move it to the top
---
v2rayN/v2rayN.Desktop/App.axaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/v2rayN/v2rayN.Desktop/App.axaml b/v2rayN/v2rayN.Desktop/App.axaml
index fb487d1d..22b10811 100644
--- a/v2rayN/v2rayN.Desktop/App.axaml
+++ b/v2rayN/v2rayN.Desktop/App.axaml
@@ -6,8 +6,8 @@
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:semi="https://irihi.tech/semi"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
- x:DataType="vms:StatusBarViewModel"
Name="v2rayN"
+ x:DataType="vms:StatusBarViewModel"
RequestedThemeVariant="Default">
@@ -32,6 +32,8 @@
ToolTipText="{Binding RunningServerToolTipText}">
+
+
-
-
From 0d74452c6c36791520778a692bffbfd90ec64a18 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sat, 21 Jun 2025 17:00:49 +0800
Subject: [PATCH 16/50] Added parameter MacOSShowInDock to control whether
MacOS platform app is displayed in the Dock
https://github.com/2dust/v2rayN/issues/7465
---
v2rayN/ServiceLib/Models/ConfigItems.cs | 1 +
v2rayN/v2rayN.Desktop/App.axaml.cs | 5 ----
v2rayN/v2rayN.Desktop/Program.cs | 40 +++++++++++++++++--------
3 files changed, 28 insertions(+), 18 deletions(-)
diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs
index afd7e7cc..08ce284b 100644
--- a/v2rayN/ServiceLib/Models/ConfigItems.cs
+++ b/v2rayN/ServiceLib/Models/ConfigItems.cs
@@ -105,6 +105,7 @@ public class UIItem
public bool Hide2TrayWhenClose { get; set; }
public List MainColumnItem { get; set; }
public bool ShowInTaskbar { get; set; }
+ public bool MacOSShowInDock { get; set; }
}
[Serializable]
diff --git a/v2rayN/v2rayN.Desktop/App.axaml.cs b/v2rayN/v2rayN.Desktop/App.axaml.cs
index 7626edfd..ebd5c29a 100644
--- a/v2rayN/v2rayN.Desktop/App.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/App.axaml.cs
@@ -11,11 +11,6 @@ public partial class App : Application
{
public override void Initialize()
{
- if (!AppHandler.Instance.InitApp())
- {
- Environment.Exit(0);
- return;
- }
AvaloniaXamlLoader.Load(this);
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
diff --git a/v2rayN/v2rayN.Desktop/Program.cs b/v2rayN/v2rayN.Desktop/Program.cs
index 15efc023..805e5a50 100644
--- a/v2rayN/v2rayN.Desktop/Program.cs
+++ b/v2rayN/v2rayN.Desktop/Program.cs
@@ -14,13 +14,17 @@ internal class Program
[STAThread]
public static void Main(string[] args)
{
- OnStartup(args);
+ if (OnStartup(args) == false)
+ {
+ Environment.Exit(0);
+ return;
+ }
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
- private static void OnStartup(string[]? Args)
+ private static bool OnStartup(string[]? Args)
{
if (Utils.IsWindows())
{
@@ -30,8 +34,7 @@ internal class Program
if (!rebootas && !bCreatedNew)
{
ProgramStarted.Set();
- Environment.Exit(0);
- return;
+ return false;
}
}
else
@@ -39,19 +42,30 @@ internal class Program
_ = new Mutex(true, "v2rayN", out var bOnlyOneInstance);
if (!bOnlyOneInstance)
{
- Environment.Exit(0);
- return;
+ return false;
}
}
+
+ if (!AppHandler.Instance.InitApp())
+ {
+ return false;
+ }
+ return true;
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
- => AppBuilder.Configure()
- .UsePlatformDetect()
- //.WithInterFont()
- .WithFontByDefault()
- .LogToTrace()
- .UseReactiveUI()
- .With(new MacOSPlatformOptions { ShowInDock = false });
+ {
+ return AppBuilder.Configure()
+ .UsePlatformDetect()
+ //.WithInterFont()
+ .WithFontByDefault()
+ .LogToTrace()
+#if OS_OSX
+ .UseReactiveUI()
+ .With(new MacOSPlatformOptions { ShowInDock = AppHandler.Instance.Config.UiItem.MacOSShowInDock });
+#else
+ .UseReactiveUI();
+#endif
+ }
}
From 7972cb8e1feefacf88930ff5bd924591d9cf9950 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 22 Jun 2025 10:31:41 +0800
Subject: [PATCH 17/50] Update winget-publish.yml
---
.github/workflows/winget-publish.yml | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/winget-publish.yml
index 5041cd5e..8ed74be1 100644
--- a/.github/workflows/winget-publish.yml
+++ b/.github/workflows/winget-publish.yml
@@ -22,10 +22,17 @@ jobs:
$github = Invoke-RestMethod -uri "https://api.github.com/repos/2dust/v2rayN/releases"
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
- $installerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-64\.zip*' | Select -ExpandProperty browser_download_url
-
+
+ $x64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-64\.zip' | Select -ExpandProperty browser_download_url
+ $arm64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-arm64\.zip' | Select -ExpandProperty browser_download_url
+
$ver = $targetRelease.tag_name
# getting latest wingetcreate file
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
- .\wingetcreate.exe update $wingetPackage -s -v $ver -u "$installerUrl|x64" -t $gitToken
+
+ Write-Host "Updating with both x64 and arm64 installers"
+ Write-Host "Version: $ver"
+ Write-Host "x64 URL: $x64InstallerUrl"
+ Write-Host "arm64 URL: $arm64InstallerUrl"
+ .\wingetcreate.exe update $wingetPackage -s -v $ver -u "$x64InstallerUrl|x64" -u "$arm64InstallerUrl|arm64" -t $gitToken
From 3df57f74ba192f3cc1a87d8b142cf44dc3fb356d Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 22 Jun 2025 10:36:05 +0800
Subject: [PATCH 18/50] Update winget-publish.yml
---
.github/workflows/winget-publish.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/winget-publish.yml
index 8ed74be1..c30b52d0 100644
--- a/.github/workflows/winget-publish.yml
+++ b/.github/workflows/winget-publish.yml
@@ -35,4 +35,5 @@ jobs:
Write-Host "Version: $ver"
Write-Host "x64 URL: $x64InstallerUrl"
Write-Host "arm64 URL: $arm64InstallerUrl"
- .\wingetcreate.exe update $wingetPackage -s -v $ver -u "$x64InstallerUrl|x64" -u "$arm64InstallerUrl|arm64" -t $gitToken
+
+ .\wingetcreate.exe update $wingetPackage -s -v $ver -u "$x64InstallerUrl|x64" "$arm64InstallerUrl|arm64" -t $gitToken
From 3e71965cd468e4d27572659bc043c3bf753b25fd Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Thu, 26 Jun 2025 14:23:30 +0800
Subject: [PATCH 19/50] =?UTF-8?q?Optimization=20and=20Improvement=EF=BC=8C?=
=?UTF-8?q?DataGrid=20list=20only=20updates=20some=20attribute=20values?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
https://github.com/2dust/v2rayN/issues/7489
---
v2rayN/ServiceLib/Models/ProfileItem.cs | 3 ++-
v2rayN/ServiceLib/Models/ProfileItemModel.cs | 17 ++++++++++++++
.../ViewModels/ProfilesViewModel.cs | 22 +++++++++----------
3 files changed, 30 insertions(+), 12 deletions(-)
diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs
index 58e9414a..67c36d8e 100644
--- a/v2rayN/ServiceLib/Models/ProfileItem.cs
+++ b/v2rayN/ServiceLib/Models/ProfileItem.cs
@@ -1,9 +1,10 @@
+using ReactiveUI;
using SQLite;
namespace ServiceLib.Models;
[Serializable]
-public class ProfileItem
+public class ProfileItem: ReactiveObject
{
public ProfileItem()
{
diff --git a/v2rayN/ServiceLib/Models/ProfileItemModel.cs b/v2rayN/ServiceLib/Models/ProfileItemModel.cs
index a1e81ca2..40ba3f1d 100644
--- a/v2rayN/ServiceLib/Models/ProfileItemModel.cs
+++ b/v2rayN/ServiceLib/Models/ProfileItemModel.cs
@@ -1,3 +1,5 @@
+using ReactiveUI.Fody.Helpers;
+
namespace ServiceLib.Models;
[Serializable]
@@ -5,13 +7,28 @@ public class ProfileItemModel : ProfileItem
{
public bool IsActive { get; set; }
public string SubRemarks { get; set; }
+
+ [Reactive]
public int Delay { get; set; }
+
public decimal Speed { get; set; }
public int Sort { get; set; }
+
+ [Reactive]
public string DelayVal { get; set; }
+
+ [Reactive]
public string SpeedVal { get; set; }
+
+ [Reactive]
public string TodayUp { get; set; }
+
+ [Reactive]
public string TodayDown { get; set; }
+
+ [Reactive]
public string TotalUp { get; set; }
+
+ [Reactive]
public string TotalDown { get; set; }
}
diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs
index a955a9aa..f312b890 100644
--- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs
@@ -304,7 +304,7 @@ public class ProfilesViewModel : MyReactiveObject
{
item.SpeedVal = result.Speed ?? string.Empty;
}
- _profileItems.Replace(item, JsonUtils.DeepCopy(item));
+ //_profileItems.Replace(item, JsonUtils.DeepCopy(item));
}
public void UpdateStatistics(ServerSpeedItem update)
@@ -319,16 +319,16 @@ public class ProfilesViewModel : MyReactiveObject
item.TotalDown = Utils.HumanFy(update.TotalDown);
item.TotalUp = Utils.HumanFy(update.TotalUp);
- if (SelectedProfile?.IndexId == item.IndexId)
- {
- var temp = JsonUtils.DeepCopy(item);
- _profileItems.Replace(item, temp);
- SelectedProfile = temp;
- }
- else
- {
- _profileItems.Replace(item, JsonUtils.DeepCopy(item));
- }
+ //if (SelectedProfile?.IndexId == item.IndexId)
+ //{
+ // var temp = JsonUtils.DeepCopy(item);
+ // _profileItems.Replace(item, temp);
+ // SelectedProfile = temp;
+ //}
+ //else
+ //{
+ // _profileItems.Replace(item, JsonUtils.DeepCopy(item));
+ //}
}
}
catch
From b3874a78b916f093ffc281fc6aa830563bb774c0 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 29 Jun 2025 11:16:21 +0800
Subject: [PATCH 20/50] Improved order of items in settings
---
.../Views/OptionSettingWindow.axaml | 52 ++++++++---------
v2rayN/v2rayN/Views/OptionSettingWindow.xaml | 56 ++++++++++---------
2 files changed, 55 insertions(+), 53 deletions(-)
diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
index 004f68a4..a75ed6fc 100644
--- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
@@ -575,9 +575,9 @@
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
- Text="{x:Static resx:ResUI.TbSettingsSubConvert}" />
-
+
+
+
+
-
-
-
diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
index f9b4b13c..584658e5 100644
--- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
+++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
@@ -846,10 +846,26 @@
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
+ Text="{x:Static resx:ResUI.TbSettingsIPAPIUrl}" />
+
+
+
-
-
+
From 30c09a7b54bdb453b6e438380b045937e45e4b11 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Sun, 29 Jun 2025 15:06:02 +0800
Subject: [PATCH 21/50] Add mux settings for per-server,
VMess/Shadowsocks/VLESS/Trojan
If you want to use global settings, do not set per-server
---
v2rayN/ServiceLib/Handler/ConfigHandler.cs | 1 +
v2rayN/ServiceLib/Models/ProfileItem.cs | 1 +
.../CoreConfig/CoreConfigSingboxService.cs | 3 +-
.../CoreConfig/CoreConfigV2rayService.cs | 2 +-
.../Views/AddServerWindow.axaml | 60 +++++++++++++++++--
.../Views/AddServerWindow.axaml.cs | 4 ++
v2rayN/v2rayN/Views/AddServerWindow.xaml | 60 +++++++++++++++++++
v2rayN/v2rayN/Views/AddServerWindow.xaml.cs | 4 ++
8 files changed, 129 insertions(+), 6 deletions(-)
diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
index 66579b92..cc8f1efc 100644
--- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs
+++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
@@ -246,6 +246,7 @@ public class ConfigHandler
item.ShortId = profileItem.ShortId;
item.SpiderX = profileItem.SpiderX;
item.Extra = profileItem.Extra;
+ item.MuxEnabled = profileItem.MuxEnabled;
}
var ret = item.ConfigType switch
diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs
index 67c36d8e..9a11d002 100644
--- a/v2rayN/ServiceLib/Models/ProfileItem.cs
+++ b/v2rayN/ServiceLib/Models/ProfileItem.cs
@@ -94,4 +94,5 @@ public class ProfileItem: ReactiveObject
public string ShortId { get; set; }
public string SpiderX { get; set; }
public string Extra { get; set; }
+ public bool? MuxEnabled { get; set; }
}
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs
index 2f3d4356..cdc2d5f5 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs
@@ -751,7 +751,8 @@ public class CoreConfigSingboxService
{
try
{
- if (_config.CoreBasicItem.MuxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
+ var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
+ if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
{
var mux = new Multiplex4Sbox()
{
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
index e1d103f2..fc10021c 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
@@ -631,7 +631,7 @@ public class CoreConfigV2rayService
{
try
{
- var muxEnabled = _config.CoreBasicItem.MuxEnabled;
+ var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
switch (node.ConfigType)
{
case EConfigType.VMess:
diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
index ef1885af..ad30f985 100644
--- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
@@ -105,7 +105,7 @@
Grid.Row="2"
ColumnDefinitions="180,Auto,Auto"
IsVisible="False"
- RowDefinitions="Auto,Auto,Auto,Auto">
+ RowDefinitions="Auto,Auto,Auto,Auto,Auto">
+
+
+
+ RowDefinitions="Auto,Auto,Auto,Auto,Auto">
+
+
+
+ RowDefinitions="Auto,Auto,Auto,Auto,Auto">
+
+
+
+ RowDefinitions="Auto,Auto,Auto,Auto,Auto">
+
+
+
this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity.SelectedValue).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables);
break;
case EConfigType.Shadowsocks:
this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId3.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity3.SelectedValue).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables);
break;
case EConfigType.SOCKS:
@@ -167,11 +169,13 @@ public partial class AddServerWindow : ReactiveWindow
this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId5.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow5.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity5.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(disposables);
break;
case EConfigType.Trojan:
this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId6.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow6.SelectedValue).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(disposables);
break;
case EConfigType.Hysteria2:
diff --git a/v2rayN/v2rayN/Views/AddServerWindow.xaml b/v2rayN/v2rayN/Views/AddServerWindow.xaml
index ba06cd3f..14b070bf 100644
--- a/v2rayN/v2rayN/Views/AddServerWindow.xaml
+++ b/v2rayN/v2rayN/Views/AddServerWindow.xaml
@@ -155,6 +155,7 @@
+
@@ -214,6 +215,20 @@
Width="200"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefComboBox}" />
+
+
+
+
@@ -259,6 +275,20 @@
Width="300"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefComboBox}" />
+
+
+
+
@@ -373,6 +404,20 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
Style="{StaticResource DefTextBox}" />
+
+
+
+
@@ -418,6 +464,20 @@
Width="200"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefComboBox}" />
+
+
+
vm.SelectedSource.Id, v => v.txtId.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables);
break;
case EConfigType.Shadowsocks:
this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId3.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity3.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables);
break;
case EConfigType.SOCKS:
@@ -161,11 +163,13 @@ public partial class AddServerWindow
this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId5.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow5.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity5.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(disposables);
break;
case EConfigType.Trojan:
this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId6.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow6.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(disposables);
break;
case EConfigType.Hysteria2:
From 84f93f2ae6730a3eb87ee30da5133792ffb12aa4 Mon Sep 17 00:00:00 2001
From: DHR60
Date: Mon, 30 Jun 2025 20:26:10 +0800
Subject: [PATCH 22/50] Optimize proxy chain handling (#7515)
* Optimize proxy chain handling
* Avoids duplicate proxy chain generation
---
.../CoreConfig/CoreConfigSingboxService.cs | 192 +++++++-------
.../CoreConfig/CoreConfigV2rayService.cs | 245 +++++++-----------
2 files changed, 183 insertions(+), 254 deletions(-)
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs
index cdc2d5f5..d81b0783 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs
@@ -918,29 +918,21 @@ public class CoreConfigSingboxService
//Previous proxy
var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
+ string? prevOutboundTag = null;
if (prevNode is not null
&& prevNode.ConfigType != EConfigType.Custom)
{
var prevOutbound = JsonUtils.Deserialize(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
- prevOutbound.tag = $"{Global.ProxyTag}2";
+ prevOutboundTag = $"prev-{Global.ProxyTag}";
+ prevOutbound.tag = prevOutboundTag;
singboxConfig.outbounds.Add(prevOutbound);
-
- outbound.detour = prevOutbound.tag;
}
+ var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag);
- //Next proxy
- var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
- if (nextNode is not null
- && nextNode.ConfigType != EConfigType.Custom)
+ if (nextOutbound is not null)
{
- var nextOutbound = JsonUtils.Deserialize(txtOutbound);
- await GenOutbound(nextNode, nextOutbound);
- nextOutbound.tag = Global.ProxyTag;
singboxConfig.outbounds.Insert(0, nextOutbound);
-
- outbound.tag = $"{Global.ProxyTag}1";
- nextOutbound.detour = outbound.tag;
}
}
catch (Exception ex)
@@ -967,8 +959,8 @@ public class CoreConfigSingboxService
var proxyTags = new List(); // For selector and urltest outbounds
// Cache for chain proxies to avoid duplicate generation
- var chainProxyCache = new Dictionary();
- var prevProxyTags = new Dictionary(); // Map from profile name to tag
+ var nextProxyCache = new Dictionary();
+ var prevProxyTags = new Dictionary(); // Map from profile name to tag
int prevIndex = 0; // Index for prev outbounds
// Process each node
@@ -977,112 +969,55 @@ public class CoreConfigSingboxService
{
index++;
- // Skip unsupported config types
- if (node.ConfigType is EConfigType.Custom)
- {
- continue;
- }
-
// Handle proxy chain
string? prevTag = null;
- Outbound4Sbox? nextOutbound = null;
-
- if (node.Subid.IsNotEmpty())
+ var currentOutbound = JsonUtils.Deserialize(txtOutbound);
+ var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null);
+ if (nextOutbound != null)
{
- // Check if chain proxy is already cached
- if (chainProxyCache.TryGetValue(node.Subid, out var chainProxy))
+ nextOutbound = JsonUtils.DeepCopy(nextOutbound);
+ }
+
+ var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
+
+ // current proxy
+ await GenOutbound(node, currentOutbound);
+ currentOutbound.tag = $"{Global.ProxyTag}-{index}";
+ proxyTags.Add(currentOutbound.tag);
+
+ if (!node.Subid.IsNullOrEmpty())
+ {
+ if (prevProxyTags.TryGetValue(node.Subid, out var value))
{
- prevTag = chainProxy.Item1;
- nextOutbound = chainProxy.Item2;
+ prevTag = value; // maybe null
}
else
{
- // Generate chain proxy and cache it
- var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
- if (subItem != null)
+ var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
+ if (prevNode is not null
+ && prevNode.ConfigType != EConfigType.Custom)
{
- // Process previous proxy
- if (!subItem.PrevProfile.IsNullOrEmpty())
- {
- // Check if this previous proxy was already created
- if (prevProxyTags.TryGetValue(subItem.PrevProfile, out var existingTag))
- {
- prevTag = existingTag;
- }
- else
- {
- var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
- if (prevNode != null && prevNode.ConfigType != EConfigType.Custom)
- {
- prevIndex++;
- var prevOutbound = JsonUtils.Deserialize(txtOutbound);
- await GenOutbound(prevNode, prevOutbound);
-
- prevTag = $"{Global.ProxyTag}-prev-{prevIndex}";
- prevOutbound.tag = prevTag;
- prevProxyTags[subItem.PrevProfile] = prevTag;
-
- // Add to prev outbounds list (will be added at the end)
- prevOutbounds.Add(prevOutbound);
- }
- }
- }
-
- // Process next proxy
- var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
- if (nextNode != null && nextNode.ConfigType != EConfigType.Custom)
- {
- nextOutbound = JsonUtils.Deserialize(txtOutbound);
- await GenOutbound(nextNode, nextOutbound);
- }
-
- // Cache the chain proxy
- chainProxyCache[node.Subid] = (prevTag, nextOutbound);
+ var prevOutbound = JsonUtils.Deserialize(txtOutbound);
+ await GenOutbound(prevNode, prevOutbound);
+ prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}";
+ prevOutbound.tag = prevTag;
+ prevOutbounds.Add(prevOutbound);
}
+ prevProxyTags[node.Subid] = prevTag;
}
- }
- // Create main outbound
- var outbound = JsonUtils.Deserialize(txtOutbound);
-
- await GenOutbound(node, outbound);
- outbound.tag = $"{Global.ProxyTag}-{index}";
-
- // Configure proxy chain relationships
- if (nextOutbound != null)
- {
- // If there's a next proxy, it should be the final outbound in the chain
- var originalTag = outbound.tag;
- outbound.tag = $"mid-{Global.ProxyTag}-{index}";
-
- var nextOutboundCopy = JsonUtils.DeepCopy(nextOutbound);
- nextOutboundCopy.tag = originalTag;
- nextOutboundCopy.detour = outbound.tag; // Use detour instead of sockopt
-
- if (prevTag != null)
+ nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound);
+ if (!nextProxyCache.ContainsKey(node.Subid))
{
- outbound.detour = prevTag;
+ nextProxyCache[node.Subid] = nextOutbound;
}
-
- // Add to proxy tags for selector/urltest
- proxyTags.Add(originalTag);
-
- // Add in reverse order to ensure final outbound is added first
- resultOutbounds.Add(nextOutboundCopy); // Final outbound (exposed to internet)
- resultOutbounds.Add(outbound); // Middle outbound
}
- else
+
+ if (nextOutbound is not null)
{
- // If no next proxy, the main outbound is the final one
- if (prevTag != null)
- {
- outbound.detour = prevTag;
- }
-
- // Add to proxy tags for selector/urltest
- proxyTags.Add(outbound.tag);
- resultOutbounds.Add(outbound);
+ resultOutbounds.Add(nextOutbound);
}
+ resultOutbounds.Add(currentOutbound);
}
// Add urltest outbound (auto selection based on latency)
@@ -1124,6 +1059,53 @@ public class CoreConfigSingboxService
return 0;
}
+ ///
+ /// Generates a chained outbound configuration for the given subItem and outbound.
+ /// The outbound's tag must be set before calling this method.
+ /// Returns the next proxy's outbound configuration, which may be null if no next proxy exists.
+ ///
+ /// The subscription item containing proxy chain information.
+ /// The current outbound configuration. Its tag must be set before calling this method.
+ /// The tag of the previous outbound in the chain, if any.
+ /// The outbound for the next proxy in the chain, if already created. If null, will be created inside.
+ ///
+ /// The outbound configuration for the next proxy in the chain, or null if no next proxy exists.
+ ///
+ private async Task GenChainOutbounds(SubItem subItem, Outbound4Sbox outbound, string? prevOutboundTag, Outbound4Sbox? nextOutbound = null)
+ {
+ try
+ {
+ var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
+
+ if (!prevOutboundTag.IsNullOrEmpty())
+ {
+ outbound.detour = prevOutboundTag;
+ }
+
+ // Next proxy
+ var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
+ if (nextNode is not null
+ && nextNode.ConfigType != EConfigType.Custom)
+ {
+ if (nextOutbound == null)
+ {
+ nextOutbound = JsonUtils.Deserialize(txtOutbound);
+ await GenOutbound(nextNode, nextOutbound);
+ }
+ nextOutbound.tag = outbound.tag;
+
+ outbound.tag = $"mid-{outbound.tag}";
+ nextOutbound.detour = outbound.tag;
+ }
+ return nextOutbound;
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(_tag, ex);
+ }
+ return null;
+ }
+
private async Task GenRouting(SingboxConfig singboxConfig)
{
try
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
index fc10021c..c181d426 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs
@@ -1318,6 +1318,7 @@ public class CoreConfigV2rayService
//Previous proxy
var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
+ string? prevOutboundTag = null;
if (prevNode is not null
&& prevNode.ConfigType != EConfigType.Custom
&& prevNode.ConfigType != EConfigType.Hysteria2
@@ -1325,32 +1326,15 @@ public class CoreConfigV2rayService
{
var prevOutbound = JsonUtils.Deserialize(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
- prevOutbound.tag = $"{Global.ProxyTag}2";
+ prevOutboundTag = $"prev-{Global.ProxyTag}";
+ prevOutbound.tag = prevOutboundTag;
v2rayConfig.outbounds.Add(prevOutbound);
-
- outbound.streamSettings.sockopt = new()
- {
- dialerProxy = prevOutbound.tag
- };
}
+ var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag);
- //Next proxy
- var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
- if (nextNode is not null
- && nextNode.ConfigType != EConfigType.Custom
- && nextNode.ConfigType != EConfigType.Hysteria2
- && nextNode.ConfigType != EConfigType.TUIC)
+ if (nextOutbound is not null)
{
- var nextOutbound = JsonUtils.Deserialize(txtOutbound);
- await GenOutbound(nextNode, nextOutbound);
- nextOutbound.tag = Global.ProxyTag;
v2rayConfig.outbounds.Insert(0, nextOutbound);
-
- outbound.tag = $"{Global.ProxyTag}1";
- nextOutbound.streamSettings.sockopt = new()
- {
- dialerProxy = outbound.tag
- };
}
}
catch (Exception ex)
@@ -1375,31 +1359,9 @@ public class CoreConfigV2rayService
var resultOutbounds = new List();
var prevOutbounds = new List(); // Separate list for prev outbounds and fragment
- // Handle fragment outbound
- Outbounds4Ray? fragmentOutbound = null;
- if (_config.CoreBasicItem.EnableFragment)
- {
- fragmentOutbound = new Outbounds4Ray
- {
- protocol = "freedom",
- tag = $"fragment-{Global.ProxyTag}",
- settings = new()
- {
- fragment = new()
- {
- packets = _config.Fragment4RayItem?.Packets,
- length = _config.Fragment4RayItem?.Length,
- interval = _config.Fragment4RayItem?.Interval
- }
- }
- };
- // Add to prevOutbounds instead of v2rayConfig.outbounds
- prevOutbounds.Add(fragmentOutbound);
- }
-
// Cache for chain proxies to avoid duplicate generation
- var chainProxyCache = new Dictionary();
- var prevProxyTags = new Dictionary(); // Map from profile name to tag
+ var nextProxyCache = new Dictionary();
+ var prevProxyTags = new Dictionary(); // Map from profile name to tag
int prevIndex = 0; // Index for prev outbounds
// Process nodes
@@ -1408,126 +1370,56 @@ public class CoreConfigV2rayService
{
index++;
- // Skip unsupported config types
- if (node.ConfigType is EConfigType.Custom or EConfigType.Hysteria2 or EConfigType.TUIC)
- {
- continue;
- }
-
// Handle proxy chain
string? prevTag = null;
- Outbounds4Ray? nextOutbound = null;
+ var currentOutbound = JsonUtils.Deserialize(txtOutbound);
+ var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null);
+ if (nextOutbound != null)
+ {
+ nextOutbound = JsonUtils.DeepCopy(nextOutbound);
+ }
+
+ var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
+
+ // current proxy
+ await GenOutbound(node, currentOutbound);
+ currentOutbound.tag = $"{Global.ProxyTag}-{index}";
if (!node.Subid.IsNullOrEmpty())
{
- // Check if chain proxy is already cached
- if (chainProxyCache.TryGetValue(node.Subid, out var chainProxy))
+ if (prevProxyTags.TryGetValue(node.Subid, out var value))
{
- prevTag = chainProxy.Item1;
- nextOutbound = chainProxy.Item2;
+ prevTag = value; // maybe null
}
else
{
- // Generate chain proxy and cache it
- var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
- if (subItem != null)
+ var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
+ if (prevNode is not null
+ && prevNode.ConfigType != EConfigType.Custom
+ && prevNode.ConfigType != EConfigType.Hysteria2
+ && prevNode.ConfigType != EConfigType.TUIC)
{
- // Process previous proxy
- if (!subItem.PrevProfile.IsNullOrEmpty())
- {
- // Check if this previous proxy was already created
- if (prevProxyTags.TryGetValue(subItem.PrevProfile, out var existingTag))
- {
- prevTag = existingTag;
- }
- else
- {
- var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
- if (prevNode != null
- && prevNode.ConfigType != EConfigType.Custom
- && prevNode.ConfigType != EConfigType.Hysteria2
- && prevNode.ConfigType != EConfigType.TUIC)
- {
- prevIndex++;
- var prevOutbound = JsonUtils.Deserialize(txtOutbound);
- await GenOutbound(prevNode, prevOutbound);
-
- prevTag = $"{Global.ProxyTag}-prev-{prevIndex}";
- prevOutbound.tag = prevTag;
- prevProxyTags[subItem.PrevProfile] = prevTag;
-
- // Set fragment if needed
- if (fragmentOutbound != null && prevOutbound.streamSettings?.security.IsNullOrEmpty() == false)
- {
- prevOutbound.streamSettings.sockopt = new()
- {
- dialerProxy = fragmentOutbound.tag
- };
- }
-
- // Add to prev outbounds list (will be added at the end)
- prevOutbounds.Add(prevOutbound);
- }
- }
- }
-
- // Process next proxy
- var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
- if (nextNode != null
- && nextNode.ConfigType != EConfigType.Custom
- && nextNode.ConfigType != EConfigType.Hysteria2
- && nextNode.ConfigType != EConfigType.TUIC)
- {
- nextOutbound = JsonUtils.Deserialize(txtOutbound);
- await GenOutbound(nextNode, nextOutbound);
- }
-
- // Cache the chain proxy
- chainProxyCache[node.Subid] = (prevTag, nextOutbound);
+ var prevOutbound = JsonUtils.Deserialize(txtOutbound);
+ await GenOutbound(prevNode, prevOutbound);
+ prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}";
+ prevOutbound.tag = prevTag;
+ prevOutbounds.Add(prevOutbound);
}
+ prevProxyTags[node.Subid] = prevTag;
+ }
+
+ nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound);
+ if (!nextProxyCache.ContainsKey(node.Subid))
+ {
+ nextProxyCache[node.Subid] = nextOutbound;
}
}
- // Create main outbound
- var outbound = JsonUtils.Deserialize(txtOutbound);
-
- await GenOutbound(node, outbound);
- outbound.tag = $"{Global.ProxyTag}-{index}";
-
- // Configure proxy chain relationships
- if (nextOutbound != null)
+ if (nextOutbound is not null)
{
- // If there's a next proxy, it should be the final outbound in the chain
- var originalTag = outbound.tag;
- outbound.tag = $"mid-{Global.ProxyTag}-{index}";
-
- var nextOutboundCopy = JsonUtils.DeepCopy(nextOutbound);
- nextOutboundCopy.tag = originalTag;
- nextOutboundCopy.streamSettings.sockopt = new() { dialerProxy = outbound.tag };
-
- if (prevTag != null)
- {
- outbound.streamSettings.sockopt = new() { dialerProxy = prevTag };
- }
-
- // Add in reverse order to ensure final outbound is added first
- resultOutbounds.Add(nextOutboundCopy); // Final outbound (exposed to internet)
- resultOutbounds.Add(outbound); // Middle outbound
- }
- else
- {
- // If no next proxy, the main outbound is the final one
- if (prevTag != null)
- {
- outbound.streamSettings.sockopt = new() { dialerProxy = prevTag };
- }
- else if (fragmentOutbound != null && outbound.streamSettings?.security.IsNullOrEmpty() == false)
- {
- outbound.streamSettings.sockopt = new() { dialerProxy = fragmentOutbound.tag };
- }
-
- resultOutbounds.Add(outbound);
+ resultOutbounds.Add(nextOutbound);
}
+ resultOutbounds.Add(currentOutbound);
}
// Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
@@ -1543,6 +1435,61 @@ public class CoreConfigV2rayService
return 0;
}
+ ///
+ /// Generates a chained outbound configuration for the given subItem and outbound.
+ /// The outbound's tag must be set before calling this method.
+ /// Returns the next proxy's outbound configuration, which may be null if no next proxy exists.
+ ///
+ /// The subscription item containing proxy chain information.
+ /// The current outbound configuration. Its tag must be set before calling this method.
+ /// The tag of the previous outbound in the chain, if any.
+ /// The outbound for the next proxy in the chain, if already created. If null, will be created inside.
+ ///
+ /// The outbound configuration for the next proxy in the chain, or null if no next proxy exists.
+ ///
+ private async Task GenChainOutbounds(SubItem subItem, Outbounds4Ray outbound, string? prevOutboundTag, Outbounds4Ray? nextOutbound = null)
+ {
+ try
+ {
+ var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
+
+ if (!prevOutboundTag.IsNullOrEmpty())
+ {
+ outbound.streamSettings.sockopt = new()
+ {
+ dialerProxy = prevOutboundTag
+ };
+ }
+
+ // Next proxy
+ var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
+ if (nextNode is not null
+ && nextNode.ConfigType != EConfigType.Custom
+ && nextNode.ConfigType != EConfigType.Hysteria2
+ && nextNode.ConfigType != EConfigType.TUIC)
+ {
+ if (nextOutbound == null)
+ {
+ nextOutbound = JsonUtils.Deserialize(txtOutbound);
+ await GenOutbound(nextNode, nextOutbound);
+ }
+ nextOutbound.tag = outbound.tag;
+
+ outbound.tag = $"mid-{outbound.tag}";
+ nextOutbound.streamSettings.sockopt = new()
+ {
+ dialerProxy = outbound.tag
+ };
+ }
+ return nextOutbound;
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(_tag, ex);
+ }
+ return null;
+ }
+
private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad)
{
if (multipleLoad == EMultipleLoad.LeastPing)
From cb28c31519b7881415a5735b72bb0c63b253fc01 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Tue, 1 Jul 2025 19:19:15 +0800
Subject: [PATCH 23/50] Remove unused
---
v2rayN/ServiceLib/Common/AesUtils.cs | 100 ---------------------------
v2rayN/ServiceLib/Common/DesUtils.cs | 74 --------------------
2 files changed, 174 deletions(-)
delete mode 100644 v2rayN/ServiceLib/Common/AesUtils.cs
delete mode 100644 v2rayN/ServiceLib/Common/DesUtils.cs
diff --git a/v2rayN/ServiceLib/Common/AesUtils.cs b/v2rayN/ServiceLib/Common/AesUtils.cs
deleted file mode 100644
index 05f1cb38..00000000
--- a/v2rayN/ServiceLib/Common/AesUtils.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using System.Security.Cryptography;
-using System.Text;
-
-namespace ServiceLib.Common;
-
-public class AesUtils
-{
- private const int KeySize = 256; // AES-256
- private const int IvSize = 16; // AES block size
- private const int Iterations = 10000;
- private static readonly byte[] Salt = Encoding.ASCII.GetBytes("saltysalt".PadRight(16, ' '));
- private static readonly string DefaultPassword = Utils.GetMd5(Utils.GetHomePath() + "AesUtils");
-
- ///
- /// Encrypt
- ///
- /// Plain text
- /// Password for key derivation or direct key in ASCII bytes
- /// Base64 encoded cipher text with IV
- public static string Encrypt(string text, string? password = null)
- {
- if (string.IsNullOrEmpty(text))
- return string.Empty;
-
- var plaintext = Encoding.UTF8.GetBytes(text);
- var key = GetKey(password);
- var iv = GenerateIv();
-
- using var aes = Aes.Create();
- aes.Key = key;
- aes.IV = iv;
-
- using var ms = new MemoryStream();
- ms.Write(iv, 0, iv.Length);
-
- using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
- {
- cs.Write(plaintext, 0, plaintext.Length);
- cs.FlushFinalBlock();
- }
-
- var cipherTextWithIv = ms.ToArray();
- return Convert.ToBase64String(cipherTextWithIv);
- }
-
- ///
- /// Decrypt
- ///
- /// Base64 encoded cipher text with IV
- /// Password for key derivation or direct key in ASCII bytes
- /// Plain text
- public static string Decrypt(string cipherTextWithIv, string? password = null)
- {
- if (string.IsNullOrEmpty(cipherTextWithIv))
- return string.Empty;
-
- var cipherTextWithIvBytes = Convert.FromBase64String(cipherTextWithIv);
- var key = GetKey(password);
-
- var iv = new byte[IvSize];
- Buffer.BlockCopy(cipherTextWithIvBytes, 0, iv, 0, IvSize);
-
- var cipherText = new byte[cipherTextWithIvBytes.Length - IvSize];
- Buffer.BlockCopy(cipherTextWithIvBytes, IvSize, cipherText, 0, cipherText.Length);
-
- using var aes = Aes.Create();
- aes.Key = key;
- aes.IV = iv;
-
- using var ms = new MemoryStream();
- using (var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
- {
- cs.Write(cipherText, 0, cipherText.Length);
- cs.FlushFinalBlock();
- }
-
- var plainText = ms.ToArray();
- return Encoding.UTF8.GetString(plainText);
- }
-
- private static byte[] GetKey(string? password)
- {
- if (password.IsNullOrEmpty())
- {
- password = DefaultPassword;
- }
-
- using var pbkdf2 = new Rfc2898DeriveBytes(password, Salt, Iterations, HashAlgorithmName.SHA256);
- return pbkdf2.GetBytes(KeySize / 8);
- }
-
- private static byte[] GenerateIv()
- {
- var randomNumber = new byte[IvSize];
-
- using var rng = RandomNumberGenerator.Create();
- rng.GetBytes(randomNumber);
- return randomNumber;
- }
-}
diff --git a/v2rayN/ServiceLib/Common/DesUtils.cs b/v2rayN/ServiceLib/Common/DesUtils.cs
deleted file mode 100644
index aae02206..00000000
--- a/v2rayN/ServiceLib/Common/DesUtils.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-using System.Security.Cryptography;
-using System.Text;
-
-namespace ServiceLib.Common;
-
-public class DesUtils
-{
- ///
- /// Encrypt
- ///
- ///
- /// ///
- ///
- public static string Encrypt(string? text, string? key = null)
- {
- if (text.IsNullOrEmpty())
- {
- return string.Empty;
- }
- GetKeyIv(key ?? GetDefaultKey(), out var rgbKey, out var rgbIv);
- var dsp = DES.Create();
- using var memStream = new MemoryStream();
- using var cryStream = new CryptoStream(memStream, dsp.CreateEncryptor(rgbKey, rgbIv), CryptoStreamMode.Write);
- using var sWriter = new StreamWriter(cryStream);
- sWriter.Write(text);
- sWriter.Flush();
- cryStream.FlushFinalBlock();
- memStream.Flush();
- return Convert.ToBase64String(memStream.GetBuffer(), 0, (int)memStream.Length);
- }
-
- ///
- /// Decrypt
- ///
- ///
- ///
- ///
- public static string Decrypt(string? encryptText, string? key = null)
- {
- if (encryptText.IsNullOrEmpty())
- {
- return string.Empty;
- }
- GetKeyIv(key ?? GetDefaultKey(), out var rgbKey, out var rgbIv);
- var dsp = DES.Create();
- var buffer = Convert.FromBase64String(encryptText);
-
- using var memStream = new MemoryStream();
- using var cryStream = new CryptoStream(memStream, dsp.CreateDecryptor(rgbKey, rgbIv), CryptoStreamMode.Write);
- cryStream.Write(buffer, 0, buffer.Length);
- cryStream.FlushFinalBlock();
- return Encoding.UTF8.GetString(memStream.ToArray());
- }
-
- private static void GetKeyIv(string key, out byte[] rgbKey, out byte[] rgbIv)
- {
- if (key.IsNullOrEmpty())
- {
- throw new ArgumentNullException("The key cannot be null");
- }
- if (key.Length <= 8)
- {
- throw new ArgumentNullException("The key length cannot be less than 8 characters.");
- }
-
- rgbKey = Encoding.ASCII.GetBytes(key.Substring(0, 8));
- rgbIv = Encoding.ASCII.GetBytes(key.Insert(0, "w").Substring(0, 8));
- }
-
- private static string GetDefaultKey()
- {
- return Utils.GetMd5(Utils.GetHomePath() + "DesUtils");
- }
-}
From 7a9ee6e9e21f5fdfa7a2dac9d5ce7a97a5a502d5 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Tue, 1 Jul 2025 19:39:27 +0800
Subject: [PATCH 24/50] Each window can remember its size
---
v2rayN/ServiceLib/Handler/ConfigHandler.cs | 31 +++++++++++
v2rayN/ServiceLib/Models/ConfigItems.cs | 13 +++--
v2rayN/ServiceLib/Models/IPAPIInfo.cs | 2 +-
v2rayN/ServiceLib/Models/ProfileItem.cs | 2 +-
v2rayN/v2rayN.Desktop/Base/WindowBase.cs | 52 +++++++++++++++++++
.../Views/AddServer2Window.axaml.cs | 4 +-
.../Views/AddServerWindow.axaml.cs | 4 +-
.../Views/DNSSettingWindow.axaml.cs | 4 +-
.../Views/GlobalHotkeySettingWindow.axaml.cs | 4 +-
.../v2rayN.Desktop/Views/MainWindow.axaml.cs | 20 ++++---
.../Views/OptionSettingWindow.axaml.cs | 4 +-
.../Views/RoutingRuleDetailsWindow.axaml.cs | 4 +-
.../Views/RoutingRuleSettingWindow.axaml.cs | 4 +-
.../Views/RoutingSettingWindow.axaml.cs | 4 +-
.../Views/SubEditWindow.axaml.cs | 4 +-
.../Views/SubSettingWindow.axaml.cs | 4 +-
v2rayN/v2rayN/App.xaml | 2 +-
v2rayN/v2rayN/Base/WindowBase.cs | 42 +++++++++++++++
v2rayN/v2rayN/Views/AddServer2Window.xaml | 5 +-
v2rayN/v2rayN/Views/AddServerWindow.xaml | 5 +-
v2rayN/v2rayN/Views/DNSSettingWindow.xaml | 5 +-
.../Views/GlobalHotkeySettingWindow.xaml | 5 +-
v2rayN/v2rayN/Views/MainWindow.xaml | 9 ++--
v2rayN/v2rayN/Views/MainWindow.xaml.cs | 22 +++-----
v2rayN/v2rayN/Views/OptionSettingWindow.xaml | 7 ++-
.../Views/RoutingRuleDetailsWindow.xaml | 5 +-
.../Views/RoutingRuleSettingWindow.xaml | 5 +-
v2rayN/v2rayN/Views/RoutingSettingWindow.xaml | 5 +-
v2rayN/v2rayN/Views/StatusBarView.xaml | 6 +--
v2rayN/v2rayN/Views/SubEditWindow.xaml | 5 +-
v2rayN/v2rayN/Views/SubSettingWindow.xaml | 5 +-
31 files changed, 212 insertions(+), 81 deletions(-)
create mode 100644 v2rayN/v2rayN.Desktop/Base/WindowBase.cs
create mode 100644 v2rayN/v2rayN/Base/WindowBase.cs
diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
index cc8f1efc..5fb5471e 100644
--- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs
+++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
@@ -101,6 +101,7 @@ public class ConfigHandler
EnableAutoAdjustMainLvColWidth = true
};
config.UiItem.MainColumnItem ??= new();
+ config.UiItem.WindowSizeItem ??= new();
if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
{
@@ -2174,4 +2175,34 @@ public class ConfigHandler
}
#endregion Regional Presets
+
+ #region UIItem
+
+ public static WindowSizeItem? GetWindowSizeItem(Config config, string typeName)
+ {
+ var sizeItem = config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName);
+ if (sizeItem == null || sizeItem.Width <= 0 || sizeItem.Height <= 0)
+ {
+ return null;
+ }
+
+ return sizeItem;
+ }
+
+ public static int SaveWindowSizeItem(Config config, string typeName, double width, double height)
+ {
+ var sizeItem = config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName);
+ if (sizeItem == null)
+ {
+ sizeItem = new WindowSizeItem { TypeName = typeName };
+ config.UiItem.WindowSizeItem.Add(sizeItem);
+ }
+
+ sizeItem.Width = width;
+ sizeItem.Height = height;
+
+ return 0;
+ }
+
+ #endregion UIItem
}
diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs
index 08ce284b..bca32a53 100644
--- a/v2rayN/ServiceLib/Models/ConfigItems.cs
+++ b/v2rayN/ServiceLib/Models/ConfigItems.cs
@@ -89,8 +89,6 @@ public class UIItem
{
public bool EnableAutoAdjustMainLvColWidth { get; set; }
public bool EnableUpdateSubOnlyRemarksExist { get; set; }
- public double MainWidth { get; set; }
- public double MainHeight { get; set; }
public double MainGirdHeight1 { get; set; }
public double MainGirdHeight2 { get; set; }
public EGirdOrientation MainGirdOrientation { get; set; } = EGirdOrientation.Vertical;
@@ -103,9 +101,10 @@ public class UIItem
public bool DoubleClick2Activate { get; set; }
public bool AutoHideStartup { get; set; }
public bool Hide2TrayWhenClose { get; set; }
- public List MainColumnItem { get; set; }
public bool ShowInTaskbar { get; set; }
public bool MacOSShowInDock { get; set; }
+ public List MainColumnItem { get; set; }
+ public List WindowSizeItem { get; set; }
}
[Serializable]
@@ -246,3 +245,11 @@ public class Fragment4RayItem
public string? Length { get; set; }
public string? Interval { get; set; }
}
+
+[Serializable]
+public class WindowSizeItem
+{
+ public string TypeName { get; set; }
+ public double Width { get; set; }
+ public double Height { get; set; }
+}
diff --git a/v2rayN/ServiceLib/Models/IPAPIInfo.cs b/v2rayN/ServiceLib/Models/IPAPIInfo.cs
index 7ba16727..86cfe111 100644
--- a/v2rayN/ServiceLib/Models/IPAPIInfo.cs
+++ b/v2rayN/ServiceLib/Models/IPAPIInfo.cs
@@ -4,7 +4,7 @@ internal class IPAPIInfo
{
public string? ip { get; set; }
public string? clientIp { get; set; }
- public string? ip_addr { get; set; }
+ public string? ip_addr { get; set; }
public string? query { get; set; }
public string? country { get; set; }
public string? country_name { get; set; }
diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs
index 9a11d002..bc4358b5 100644
--- a/v2rayN/ServiceLib/Models/ProfileItem.cs
+++ b/v2rayN/ServiceLib/Models/ProfileItem.cs
@@ -4,7 +4,7 @@ using SQLite;
namespace ServiceLib.Models;
[Serializable]
-public class ProfileItem: ReactiveObject
+public class ProfileItem : ReactiveObject
{
public ProfileItem()
{
diff --git a/v2rayN/v2rayN.Desktop/Base/WindowBase.cs b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs
new file mode 100644
index 00000000..ecdeb24f
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs
@@ -0,0 +1,52 @@
+using Avalonia;
+using Avalonia.Interactivity;
+using Avalonia.ReactiveUI;
+
+namespace v2rayN.Desktop.Base;
+
+public class WindowBase : ReactiveWindow where TViewModel : class
+{
+ public WindowBase()
+ {
+ Loaded += OnLoaded;
+ }
+
+ private void ReactiveWindowBase_Closed(object? sender, EventArgs e)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected virtual void OnLoaded(object? sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var sizeItem = ConfigHandler.GetWindowSizeItem(AppHandler.Instance.Config, GetType().Name);
+ if (sizeItem == null)
+ {
+ return;
+ }
+
+ Width = sizeItem.Width;
+ Height = sizeItem.Height;
+
+ var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea;
+ var scaling = VisualRoot is not null ? VisualRoot.RenderScaling : 1.0;
+
+ var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2);
+ var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2);
+
+ Position = new PixelPoint((int)x, (int)y);
+ }
+ catch { }
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ base.OnClosed(e);
+ try
+ {
+ ConfigHandler.SaveWindowSizeItem(AppHandler.Instance.Config, GetType().Name, Width, Height);
+ }
+ catch { }
+ }
+}
diff --git a/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml.cs b/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml.cs
index 79b6ce7d..3c96871f 100644
--- a/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml.cs
@@ -1,12 +1,12 @@
using System.Reactive.Disposables;
using Avalonia.Interactivity;
-using Avalonia.ReactiveUI;
using ReactiveUI;
+using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views;
-public partial class AddServer2Window : ReactiveWindow
+public partial class AddServer2Window : WindowBase
{
public AddServer2Window()
{
diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs
index d7cf39e2..64c43f6c 100644
--- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs
@@ -1,12 +1,12 @@
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Interactivity;
-using Avalonia.ReactiveUI;
using ReactiveUI;
+using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views;
-public partial class AddServerWindow : ReactiveWindow
+public partial class AddServerWindow : WindowBase
{
public AddServerWindow()
{
diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs
index c687e153..347093f9 100644
--- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs
@@ -1,11 +1,11 @@
using System.Reactive.Disposables;
using Avalonia.Interactivity;
-using Avalonia.ReactiveUI;
using ReactiveUI;
+using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views;
-public partial class DNSSettingWindow : ReactiveWindow
+public partial class DNSSettingWindow : WindowBase
{
private static Config _config;
diff --git a/v2rayN/v2rayN.Desktop/Views/GlobalHotkeySettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/GlobalHotkeySettingWindow.axaml.cs
index f73a49d9..a9f79765 100644
--- a/v2rayN/v2rayN.Desktop/Views/GlobalHotkeySettingWindow.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/GlobalHotkeySettingWindow.axaml.cs
@@ -3,13 +3,13 @@ using System.Text;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
-using Avalonia.ReactiveUI;
using ReactiveUI;
+using v2rayN.Desktop.Base;
using v2rayN.Desktop.Handler;
namespace v2rayN.Desktop.Views;
-public partial class GlobalHotkeySettingWindow : ReactiveWindow
+public partial class GlobalHotkeySettingWindow : WindowBase
{
private readonly List