mirror of
https://github.com/2dust/v2rayN.git
synced 2025-12-18 06:19:49 +08:00
* Multi Profile * VM and wpf * avalonia * Fix right click not working * Exclude specific profile types from selection * Rename * Add Policy Group support * Add generate policy group * Adjust UI * Add Proxy Chain support * Fix * Add fallback support * Add PolicyGroup include other Group support * Add group in traffic splitting support * Avoid duplicate tags * Refactor * Adjust chained proxy, actual outbound is at the top Based on actual network flow instead of data packets * Add helper function * Refactor * Add chain selection control to group outbounds * Avoid self-reference * Fix * Improves Tun2Socks address handling * Avoids circular dependency in profile groups Adds cycle detection to prevent infinite loops when evaluating profile groups. This ensures that profile group configurations don't result in stack overflow errors when groups reference each other, directly or indirectly. * Fix * Fix * Update ProfileGroupItem.cs * Refactor * Remove unnecessary checks --------- Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
454 lines
18 KiB
C#
454 lines
18 KiB
C#
using System.ComponentModel;
|
|
using System.Reactive.Disposables;
|
|
using System.Reactive.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Media;
|
|
using MaterialDesignThemes.Wpf;
|
|
using ReactiveUI;
|
|
using v2rayN.Manager;
|
|
|
|
namespace v2rayN.Views;
|
|
|
|
public partial class MainWindow
|
|
{
|
|
private static Config _config;
|
|
private CheckUpdateView? _checkUpdateView;
|
|
private BackupAndRestoreView? _backupAndRestoreView;
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
|
|
_config = AppManager.Instance.Config;
|
|
ThreadPool.RegisterWaitForSingleObject(App.ProgramStarted, OnProgramStarted, null, -1, false);
|
|
|
|
App.Current.SessionEnding += Current_SessionEnding;
|
|
this.Closing += MainWindow_Closing;
|
|
this.PreviewKeyDown += MainWindow_PreviewKeyDown;
|
|
menuSettingsSetUWP.Click += menuSettingsSetUWP_Click;
|
|
menuPromotion.Click += menuPromotion_Click;
|
|
menuClose.Click += menuClose_Click;
|
|
menuCheckUpdate.Click += MenuCheckUpdate_Click;
|
|
menuBackupAndRestore.Click += MenuBackupAndRestore_Click;
|
|
|
|
ViewModel = new MainWindowViewModel(UpdateViewHandler);
|
|
|
|
switch (_config.UiItem.MainGirdOrientation)
|
|
{
|
|
case EGirdOrientation.Horizontal:
|
|
tabProfiles.Content ??= new ProfilesView();
|
|
tabMsgView.Content ??= new MsgView();
|
|
tabClashProxies.Content ??= new ClashProxiesView();
|
|
tabClashConnections.Content ??= new ClashConnectionsView();
|
|
gridMain.Visibility = Visibility.Visible;
|
|
break;
|
|
|
|
case EGirdOrientation.Vertical:
|
|
tabProfiles1.Content ??= new ProfilesView();
|
|
tabMsgView1.Content ??= new MsgView();
|
|
tabClashProxies1.Content ??= new ClashProxiesView();
|
|
tabClashConnections1.Content ??= new ClashConnectionsView();
|
|
gridMain1.Visibility = Visibility.Visible;
|
|
break;
|
|
|
|
case EGirdOrientation.Tab:
|
|
default:
|
|
tabProfiles2.Content ??= new ProfilesView();
|
|
tabMsgView2.Content ??= new MsgView();
|
|
tabClashProxies2.Content ??= new ClashProxiesView();
|
|
tabClashConnections2.Content ??= new ClashConnectionsView();
|
|
gridMain2.Visibility = Visibility.Visible;
|
|
break;
|
|
}
|
|
pbTheme.Content ??= new ThemeSettingView();
|
|
|
|
this.WhenActivated(disposables =>
|
|
{
|
|
//servers
|
|
this.BindCommand(ViewModel, vm => vm.AddVmessServerCmd, v => v.menuAddVmessServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddVlessServerCmd, v => v.menuAddVlessServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddShadowsocksServerCmd, v => v.menuAddShadowsocksServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddSocksServerCmd, v => v.menuAddSocksServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddHttpServerCmd, v => v.menuAddHttpServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddTrojanServerCmd, v => v.menuAddTrojanServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddHysteria2ServerCmd, v => v.menuAddHysteria2Server).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddTuicServerCmd, v => v.menuAddTuicServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.AddServerViaImageCmd, v => v.menuAddServerViaImage).DisposeWith(disposables);
|
|
|
|
//sub
|
|
this.BindCommand(ViewModel, vm => vm.SubSettingCmd, v => v.menuSubSetting).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.SubUpdateCmd, v => v.menuSubUpdate).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.SubUpdateViaProxyCmd, v => v.menuSubUpdateViaProxy).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.SubGroupUpdateCmd, v => v.menuSubGroupUpdate).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.SubGroupUpdateViaProxyCmd, v => v.menuSubGroupUpdateViaProxy).DisposeWith(disposables);
|
|
|
|
//setting
|
|
this.BindCommand(ViewModel, vm => vm.OptionSettingCmd, v => v.menuOptionSetting).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.RoutingSettingCmd, v => v.menuRoutingSetting).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.DNSSettingCmd, v => v.menuDNSSetting).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.FullConfigTemplateCmd, v => v.menuFullConfigTemplate).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.GlobalHotkeySettingCmd, v => v.menuGlobalHotkeySetting).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.RebootAsAdminCmd, v => v.menuRebootAsAdmin).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.ClearServerStatisticsCmd, v => v.menuClearServerStatistics).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.OpenTheFileLocationCmd, v => v.menuOpenTheFileLocation).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.RegionalPresetDefaultCmd, v => v.menuRegionalPresetsDefault).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.RegionalPresetRussiaCmd, v => v.menuRegionalPresetsRussia).DisposeWith(disposables);
|
|
this.BindCommand(ViewModel, vm => vm.RegionalPresetIranCmd, v => v.menuRegionalPresetsIran).DisposeWith(disposables);
|
|
|
|
this.BindCommand(ViewModel, vm => vm.ReloadCmd, v => v.menuReload).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.BlReloadEnabled, v => v.menuReload.IsEnabled).DisposeWith(disposables);
|
|
|
|
switch (_config.UiItem.MainGirdOrientation)
|
|
{
|
|
case EGirdOrientation.Horizontal:
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView.Visibility).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies.Visibility).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections.Visibility).DisposeWith(disposables);
|
|
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain.SelectedIndex).DisposeWith(disposables);
|
|
break;
|
|
|
|
case EGirdOrientation.Vertical:
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView1.Visibility).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies1.Visibility).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections1.Visibility).DisposeWith(disposables);
|
|
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain1.SelectedIndex).DisposeWith(disposables);
|
|
break;
|
|
|
|
case EGirdOrientation.Tab:
|
|
default:
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies2.Visibility).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections2.Visibility).DisposeWith(disposables);
|
|
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain2.SelectedIndex).DisposeWith(disposables);
|
|
break;
|
|
}
|
|
|
|
AppEvents.SendSnackMsgRequested
|
|
.AsObservable()
|
|
.ObserveOn(RxApp.MainThreadScheduler)
|
|
.Subscribe(async content => await DelegateSnackMsg(content))
|
|
.DisposeWith(disposables);
|
|
|
|
AppEvents.AppExitRequested
|
|
.AsObservable()
|
|
.ObserveOn(RxApp.MainThreadScheduler)
|
|
.Subscribe(_ => StorageUI())
|
|
.DisposeWith(disposables);
|
|
|
|
AppEvents.ShutdownRequested
|
|
.AsObservable()
|
|
.ObserveOn(RxApp.MainThreadScheduler)
|
|
.Subscribe(content => Shutdown(content))
|
|
.DisposeWith(disposables);
|
|
|
|
AppEvents.ShowHideWindowRequested
|
|
.AsObservable()
|
|
.ObserveOn(RxApp.MainThreadScheduler)
|
|
.Subscribe(blShow => ShowHideWindow(blShow))
|
|
.DisposeWith(disposables);
|
|
});
|
|
|
|
this.Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}";
|
|
|
|
if (!_config.GuiItem.EnableHWA)
|
|
{
|
|
RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly;
|
|
}
|
|
|
|
AddHelpMenuItem();
|
|
WindowsManager.Instance.RegisterGlobalHotkey(_config, OnHotkeyHandler, null);
|
|
}
|
|
|
|
#region Event
|
|
|
|
private void OnProgramStarted(object state, bool timeout)
|
|
{
|
|
Application.Current?.Dispatcher.Invoke((Action)(() =>
|
|
{
|
|
ShowHideWindow(true);
|
|
}));
|
|
}
|
|
|
|
private async Task DelegateSnackMsg(string content)
|
|
{
|
|
MainSnackbar.MessageQueue?.Enqueue(content);
|
|
}
|
|
|
|
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
|
{
|
|
switch (action)
|
|
{
|
|
case EViewAction.AddServerWindow:
|
|
if (obj is null)
|
|
return false;
|
|
return (new AddServerWindow((ProfileItem)obj)).ShowDialog() ?? false;
|
|
|
|
case EViewAction.AddServer2Window:
|
|
if (obj is null)
|
|
return false;
|
|
return (new AddServer2Window((ProfileItem)obj)).ShowDialog() ?? false;
|
|
|
|
case EViewAction.AddGroupServerWindow:
|
|
if (obj is null)
|
|
return false;
|
|
return (new AddGroupServerWindow((ProfileItem)obj)).ShowDialog() ?? false;
|
|
|
|
case EViewAction.DNSSettingWindow:
|
|
return (new DNSSettingWindow().ShowDialog() ?? false);
|
|
|
|
case EViewAction.RoutingSettingWindow:
|
|
return (new RoutingSettingWindow().ShowDialog() ?? false);
|
|
|
|
case EViewAction.OptionSettingWindow:
|
|
return (new OptionSettingWindow().ShowDialog() ?? false);
|
|
|
|
case EViewAction.FullConfigTemplateWindow:
|
|
return (new FullConfigTemplateWindow().ShowDialog() ?? false);
|
|
|
|
case EViewAction.GlobalHotkeySettingWindow:
|
|
return (new GlobalHotkeySettingWindow().ShowDialog() ?? false);
|
|
|
|
case EViewAction.SubSettingWindow:
|
|
return (new SubSettingWindow().ShowDialog() ?? false);
|
|
|
|
case EViewAction.ScanScreenTask:
|
|
await ScanScreenTaskAsync();
|
|
break;
|
|
|
|
case EViewAction.ScanImageTask:
|
|
await ScanImageTaskAsync();
|
|
break;
|
|
|
|
case EViewAction.AddServerViaClipboard:
|
|
await AddServerViaClipboardAsync();
|
|
break;
|
|
}
|
|
|
|
return await Task.FromResult(true);
|
|
}
|
|
|
|
private void OnHotkeyHandler(EGlobalHotkey e)
|
|
{
|
|
switch (e)
|
|
{
|
|
case EGlobalHotkey.ShowForm:
|
|
ShowHideWindow(null);
|
|
break;
|
|
|
|
case EGlobalHotkey.SystemProxyClear:
|
|
case EGlobalHotkey.SystemProxySet:
|
|
case EGlobalHotkey.SystemProxyUnchanged:
|
|
case EGlobalHotkey.SystemProxyPac:
|
|
AppEvents.SysProxyChangeRequested.Publish((ESysProxyType)((int)e - 1));
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void MainWindow_Closing(object? sender, CancelEventArgs e)
|
|
{
|
|
e.Cancel = true;
|
|
ShowHideWindow(false);
|
|
}
|
|
|
|
private async void Current_SessionEnding(object sender, SessionEndingCancelEventArgs e)
|
|
{
|
|
Logging.SaveLog("Current_SessionEnding");
|
|
StorageUI();
|
|
await AppManager.Instance.AppExitAsync(false);
|
|
}
|
|
|
|
private void Shutdown(bool obj)
|
|
{
|
|
Application.Current.Shutdown();
|
|
}
|
|
|
|
private void MainWindow_PreviewKeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
|
|
{
|
|
switch (e.Key)
|
|
{
|
|
case Key.V:
|
|
if (Keyboard.FocusedElement is TextBox)
|
|
{
|
|
return;
|
|
}
|
|
AddServerViaClipboardAsync().ContinueWith(_ => { });
|
|
|
|
break;
|
|
|
|
case Key.S:
|
|
ScanScreenTaskAsync().ContinueWith(_ => { });
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (e.Key == Key.F5)
|
|
{
|
|
ViewModel?.Reload();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void menuClose_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
StorageUI();
|
|
ShowHideWindow(false);
|
|
}
|
|
|
|
private void menuPromotion_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
ProcUtils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}");
|
|
}
|
|
|
|
private void menuSettingsSetUWP_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe"));
|
|
}
|
|
|
|
public async Task AddServerViaClipboardAsync()
|
|
{
|
|
var clipboardData = WindowsUtils.GetClipboardData();
|
|
if (clipboardData.IsNotEmpty() && ViewModel != null)
|
|
{
|
|
await ViewModel.AddServerViaClipboardAsync(clipboardData);
|
|
}
|
|
}
|
|
|
|
private async Task ScanScreenTaskAsync()
|
|
{
|
|
ShowHideWindow(false);
|
|
|
|
if (Application.Current?.MainWindow is Window window)
|
|
{
|
|
var bytes = QRCodeUtils.CaptureScreen(window);
|
|
await ViewModel?.ScanScreenResult(bytes);
|
|
}
|
|
|
|
ShowHideWindow(true);
|
|
}
|
|
|
|
private async Task ScanImageTaskAsync()
|
|
{
|
|
if (UI.OpenFileDialog(out var fileName, "PNG|*.png|All|*.*") != true)
|
|
{
|
|
return;
|
|
}
|
|
if (fileName.IsNullOrEmpty())
|
|
{
|
|
return;
|
|
}
|
|
await ViewModel?.ScanImageResult(fileName);
|
|
}
|
|
|
|
private void MenuCheckUpdate_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_checkUpdateView ??= new CheckUpdateView();
|
|
DialogHost.Show(_checkUpdateView, "RootDialog");
|
|
}
|
|
|
|
private void MenuBackupAndRestore_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_backupAndRestoreView ??= new BackupAndRestoreView();
|
|
DialogHost.Show(_backupAndRestoreView, "RootDialog");
|
|
}
|
|
|
|
#endregion Event
|
|
|
|
#region UI
|
|
|
|
public void ShowHideWindow(bool? blShow)
|
|
{
|
|
var bl = blShow ?? !_config.UiItem.ShowInTaskbar;
|
|
if (bl)
|
|
{
|
|
this?.Show();
|
|
if (this?.WindowState == WindowState.Minimized)
|
|
{
|
|
this.WindowState = WindowState.Normal;
|
|
}
|
|
this?.Activate();
|
|
this?.Focus();
|
|
}
|
|
else
|
|
{
|
|
this?.Hide();
|
|
}
|
|
_config.UiItem.ShowInTaskbar = bl;
|
|
}
|
|
|
|
protected override void OnLoaded(object? sender, RoutedEventArgs e)
|
|
{
|
|
base.OnLoaded(sender, e);
|
|
RestoreUI();
|
|
}
|
|
|
|
private void RestoreUI()
|
|
{
|
|
if (_config.UiItem.MainGirdHeight1 > 0 && _config.UiItem.MainGirdHeight2 > 0)
|
|
{
|
|
if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal)
|
|
{
|
|
gridMain.ColumnDefinitions[0].Width = new GridLength(_config.UiItem.MainGirdHeight1, GridUnitType.Star);
|
|
gridMain.ColumnDefinitions[2].Width = new GridLength(_config.UiItem.MainGirdHeight2, GridUnitType.Star);
|
|
}
|
|
else if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Vertical)
|
|
{
|
|
gridMain1.RowDefinitions[0].Height = new GridLength(_config.UiItem.MainGirdHeight1, GridUnitType.Star);
|
|
gridMain1.RowDefinitions[2].Height = new GridLength(_config.UiItem.MainGirdHeight2, GridUnitType.Star);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void StorageUI()
|
|
{
|
|
ConfigHandler.SaveWindowSizeItem(_config, GetType().Name, Width, Height);
|
|
|
|
if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal)
|
|
{
|
|
ConfigHandler.SaveMainGirdHeight(_config, gridMain.ColumnDefinitions[0].ActualWidth, gridMain.ColumnDefinitions[2].ActualWidth);
|
|
}
|
|
else if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Vertical)
|
|
{
|
|
ConfigHandler.SaveMainGirdHeight(_config, gridMain1.RowDefinitions[0].ActualHeight, gridMain1.RowDefinitions[2].ActualHeight);
|
|
}
|
|
}
|
|
|
|
private void AddHelpMenuItem()
|
|
{
|
|
var coreInfo = CoreInfoManager.Instance.GetCoreInfo();
|
|
foreach (var it in coreInfo
|
|
.Where(t => t.CoreType != ECoreType.v2fly
|
|
&& t.CoreType != ECoreType.hysteria))
|
|
{
|
|
var item = new MenuItem()
|
|
{
|
|
Tag = it.Url.Replace(@"/releases", ""),
|
|
Header = string.Format(ResUI.menuWebsiteItem, it.CoreType.ToString().Replace("_", " ")).UpperFirstChar()
|
|
};
|
|
item.Click += MenuItem_Click;
|
|
menuHelp.Items.Add(item);
|
|
}
|
|
}
|
|
|
|
private void MenuItem_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is MenuItem item)
|
|
{
|
|
ProcUtils.ProcessStart(item.Tag.ToString());
|
|
}
|
|
}
|
|
|
|
#endregion UI
|
|
}
|