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 _textBoxKeyEventItem = new(); diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs index 6aa89db6..63646201 100644 --- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs @@ -5,18 +5,18 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.ReactiveUI; using Avalonia.Threading; using DialogHostAvalonia; using MsBox.Avalonia.Enums; using ReactiveUI; using Splat; +using v2rayN.Desktop.Base; using v2rayN.Desktop.Common; using v2rayN.Desktop.Handler; namespace v2rayN.Desktop.Views; -public partial class MainWindow : ReactiveWindow +public partial class MainWindow : WindowBase { private static Config _config; private WindowNotificationManager? _manager; @@ -154,7 +154,6 @@ public partial class MainWindow : ReactiveWindow } menuAddServerViaScan.IsVisible = false; - RestoreUI(); AddHelpMenuItem(); MessageBus.Current.Listen(EMsgCommand.AppExit.ToString()).Subscribe(StorageUI); } @@ -436,14 +435,14 @@ public partial class MainWindow : ReactiveWindow _config.UiItem.ShowInTaskbar = bl; } + protected override void OnLoaded(object? sender, RoutedEventArgs e) + { + base.OnLoaded(sender, e); + RestoreUI(); + } + private void RestoreUI() { - if (_config.UiItem.MainWidth > 0 && _config.UiItem.MainHeight > 0) - { - Width = _config.UiItem.MainWidth; - Height = _config.UiItem.MainHeight; - } - if (_config.UiItem.MainGirdHeight1 > 0 && _config.UiItem.MainGirdHeight2 > 0) { if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal) @@ -461,8 +460,7 @@ public partial class MainWindow : ReactiveWindow private void StorageUI(string? n = null) { - _config.UiItem.MainWidth = this.Width; - _config.UiItem.MainHeight = this.Height; + ConfigHandler.SaveWindowSizeItem(_config, GetType().Name, Width, Height); if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal) { diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs index fd319d92..f417e498 100644 --- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs @@ -1,11 +1,11 @@ using System.Reactive.Disposables; using Avalonia.Controls; -using Avalonia.ReactiveUI; using ReactiveUI; +using v2rayN.Desktop.Base; namespace v2rayN.Desktop.Views; -public partial class OptionSettingWindow : ReactiveWindow +public partial class OptionSettingWindow : WindowBase { private static Config _config; diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml.cs index c6eead9f..35bc24a4 100644 --- a/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.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 RoutingRuleDetailsWindow : ReactiveWindow +public partial class RoutingRuleDetailsWindow : WindowBase { public RoutingRuleDetailsWindow() { diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs index 1cae4c0b..37b9a1a8 100644 --- a/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs @@ -2,14 +2,14 @@ using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.ReactiveUI; using MsBox.Avalonia.Enums; using ReactiveUI; +using v2rayN.Desktop.Base; using v2rayN.Desktop.Common; namespace v2rayN.Desktop.Views; -public partial class RoutingRuleSettingWindow : ReactiveWindow +public partial class RoutingRuleSettingWindow : WindowBase { public RoutingRuleSettingWindow() { diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs index 2f187f8e..0e6f69cc 100644 --- a/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs @@ -2,14 +2,14 @@ using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.ReactiveUI; using MsBox.Avalonia.Enums; using ReactiveUI; +using v2rayN.Desktop.Base; using v2rayN.Desktop.Common; namespace v2rayN.Desktop.Views; -public partial class RoutingSettingWindow : ReactiveWindow +public partial class RoutingSettingWindow : WindowBase { private bool _manualClose = false; diff --git a/v2rayN/v2rayN.Desktop/Views/SubEditWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/SubEditWindow.axaml.cs index 6c0f192e..b2fcf442 100644 --- a/v2rayN/v2rayN.Desktop/Views/SubEditWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/SubEditWindow.axaml.cs @@ -1,12 +1,12 @@ using System.Reactive.Disposables; using Avalonia; using Avalonia.Interactivity; -using Avalonia.ReactiveUI; using ReactiveUI; +using v2rayN.Desktop.Base; namespace v2rayN.Desktop.Views; -public partial class SubEditWindow : ReactiveWindow +public partial class SubEditWindow : WindowBase { public SubEditWindow() { diff --git a/v2rayN/v2rayN.Desktop/Views/SubSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/SubSettingWindow.axaml.cs index 7882449e..9ff0bcbd 100644 --- a/v2rayN/v2rayN.Desktop/Views/SubSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/SubSettingWindow.axaml.cs @@ -1,16 +1,16 @@ using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Interactivity; -using Avalonia.ReactiveUI; using DialogHostAvalonia; using DynamicData; using MsBox.Avalonia.Enums; using ReactiveUI; +using v2rayN.Desktop.Base; using v2rayN.Desktop.Common; namespace v2rayN.Desktop.Views; -public partial class SubSettingWindow : ReactiveWindow +public partial class SubSettingWindow : WindowBase { private bool _manualClose = false; diff --git a/v2rayN/v2rayN/App.xaml b/v2rayN/v2rayN/App.xaml index 49a31268..e6b7e3af 100644 --- a/v2rayN/v2rayN/App.xaml +++ b/v2rayN/v2rayN/App.xaml @@ -215,7 +215,7 @@ - +