mirror of
https://github.com/2dust/v2rayN.git
synced 2026-05-18 23:54:49 +03:00
11343a30fd
* 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>
502 lines
20 KiB
C#
502 lines
20 KiB
C#
using System.Reactive.Disposables;
|
|
using System.Reactive.Linq;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
|
using Avalonia.Controls.Notifications;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Threading;
|
|
using DialogHostAvalonia;
|
|
using MsBox.Avalonia.Enums;
|
|
using ReactiveUI;
|
|
using v2rayN.Desktop.Base;
|
|
using v2rayN.Desktop.Common;
|
|
using v2rayN.Desktop.Manager;
|
|
|
|
namespace v2rayN.Desktop.Views;
|
|
|
|
public partial class MainWindow : WindowBase<MainWindowViewModel>
|
|
{
|
|
private static Config _config;
|
|
private readonly WindowNotificationManager? _manager;
|
|
private CheckUpdateView? _checkUpdateView;
|
|
private BackupAndRestoreView? _backupAndRestoreView;
|
|
private bool _blCloseByUser = false;
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
|
|
_config = AppManager.Instance.Config;
|
|
_manager = new WindowNotificationManager(TopLevel.GetTopLevel(this)) { MaxItems = 3, Position = NotificationPosition.TopRight };
|
|
|
|
this.KeyDown += MainWindow_KeyDown;
|
|
menuSettingsSetUWP.Click += menuSettingsSetUWP_Click;
|
|
menuPromotion.Click += menuPromotion_Click;
|
|
menuCheckUpdate.Click += MenuCheckUpdate_Click;
|
|
menuBackupAndRestore.Click += MenuBackupAndRestore_Click;
|
|
menuClose.Click += MenuClose_Click;
|
|
|
|
ViewModel = new MainWindowViewModel(UpdateViewHandler);
|
|
|
|
switch (_config.UiItem.MainGirdOrientation)
|
|
{
|
|
case EGirdOrientation.Horizontal:
|
|
tabProfiles.Content ??= new ProfilesView(this);
|
|
tabMsgView.Content ??= new MsgView();
|
|
tabClashProxies.Content ??= new ClashProxiesView();
|
|
tabClashConnections.Content ??= new ClashConnectionsView();
|
|
gridMain.IsVisible = true;
|
|
break;
|
|
|
|
case EGirdOrientation.Vertical:
|
|
tabProfiles1.Content ??= new ProfilesView(this);
|
|
tabMsgView1.Content ??= new MsgView();
|
|
tabClashProxies1.Content ??= new ClashProxiesView();
|
|
tabClashConnections1.Content ??= new ClashConnectionsView();
|
|
gridMain1.IsVisible = true;
|
|
break;
|
|
|
|
case EGirdOrientation.Tab:
|
|
default:
|
|
tabProfiles2.Content ??= new ProfilesView(this);
|
|
tabMsgView2.Content ??= new MsgView();
|
|
tabClashProxies2.Content ??= new ClashProxiesView();
|
|
tabClashConnections2.Content ??= new ClashConnectionsView();
|
|
gridMain2.IsVisible = true;
|
|
break;
|
|
}
|
|
conTheme.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.IsVisible).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies.IsVisible).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections.IsVisible).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.IsVisible).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies1.IsVisible).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections1.IsVisible).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.IsVisible).DisposeWith(disposables);
|
|
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections2.IsVisible).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);
|
|
});
|
|
|
|
if (Utils.IsWindows())
|
|
{
|
|
this.Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}";
|
|
|
|
ThreadPool.RegisterWaitForSingleObject(Program.ProgramStarted, OnProgramStarted, null, -1, false);
|
|
HotkeyManager.Instance.Init(_config, OnHotkeyHandler);
|
|
}
|
|
else
|
|
{
|
|
this.Title = $"{Utils.GetVersion()}";
|
|
|
|
menuRebootAsAdmin.IsVisible = false;
|
|
menuSettingsSetUWP.IsVisible = false;
|
|
menuGlobalHotkeySetting.IsVisible = false;
|
|
}
|
|
menuAddServerViaScan.IsVisible = false;
|
|
|
|
AddHelpMenuItem();
|
|
}
|
|
|
|
#region Event
|
|
|
|
private void OnProgramStarted(object state, bool timeout)
|
|
{
|
|
Dispatcher.UIThread.Post(() =>
|
|
ShowHideWindow(true),
|
|
DispatcherPriority.Default);
|
|
}
|
|
|
|
private async Task DelegateSnackMsg(string content)
|
|
{
|
|
_manager?.Show(new Notification(null, content, NotificationType.Information));
|
|
}
|
|
|
|
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
|
{
|
|
switch (action)
|
|
{
|
|
case EViewAction.AddServerWindow:
|
|
if (obj is null)
|
|
return false;
|
|
return await new AddServerWindow((ProfileItem)obj).ShowDialog<bool>(this);
|
|
|
|
case EViewAction.AddServer2Window:
|
|
if (obj is null)
|
|
return false;
|
|
return await new AddServer2Window((ProfileItem)obj).ShowDialog<bool>(this);
|
|
|
|
case EViewAction.AddGroupServerWindow:
|
|
if (obj is null)
|
|
return false;
|
|
return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog<bool>(this);
|
|
|
|
case EViewAction.DNSSettingWindow:
|
|
return await new DNSSettingWindow().ShowDialog<bool>(this);
|
|
|
|
case EViewAction.FullConfigTemplateWindow:
|
|
return await new FullConfigTemplateWindow().ShowDialog<bool>(this);
|
|
|
|
case EViewAction.RoutingSettingWindow:
|
|
return await new RoutingSettingWindow().ShowDialog<bool>(this);
|
|
|
|
case EViewAction.OptionSettingWindow:
|
|
return await new OptionSettingWindow().ShowDialog<bool>(this);
|
|
|
|
case EViewAction.GlobalHotkeySettingWindow:
|
|
return await new GlobalHotkeySettingWindow().ShowDialog<bool>(this);
|
|
|
|
case EViewAction.SubSettingWindow:
|
|
return await new SubSettingWindow().ShowDialog<bool>(this);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
protected override async void OnClosing(WindowClosingEventArgs e)
|
|
{
|
|
if (_blCloseByUser)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Logging.SaveLog("OnClosing -> " + e.CloseReason.ToString());
|
|
|
|
switch (e.CloseReason)
|
|
{
|
|
case WindowCloseReason.OwnerWindowClosing or WindowCloseReason.WindowClosing:
|
|
e.Cancel = true;
|
|
ShowHideWindow(false);
|
|
break;
|
|
|
|
case WindowCloseReason.ApplicationShutdown or WindowCloseReason.OSShutdown:
|
|
await AppManager.Instance.AppExitAsync(false);
|
|
break;
|
|
}
|
|
|
|
base.OnClosing(e);
|
|
}
|
|
|
|
private async void MainWindow_KeyDown(object? sender, KeyEventArgs e)
|
|
{
|
|
if (e.KeyModifiers is KeyModifiers.Control or KeyModifiers.Meta)
|
|
{
|
|
switch (e.Key)
|
|
{
|
|
case Key.V:
|
|
await AddServerViaClipboardAsync();
|
|
break;
|
|
|
|
case Key.S:
|
|
await ScanScreenTaskAsync();
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (e.Key == Key.F5)
|
|
{
|
|
ViewModel?.Reload();
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = await AvaUtils.GetClipboardData(this);
|
|
if (clipboardData.IsNotEmpty() && ViewModel != null)
|
|
{
|
|
await ViewModel.AddServerViaClipboardAsync(clipboardData);
|
|
}
|
|
}
|
|
|
|
public async Task ScanScreenTaskAsync()
|
|
{
|
|
//ShowHideWindow(false);
|
|
|
|
NoticeManager.Instance.SendMessageAndEnqueue("Not yet implemented.(还未实现)");
|
|
await Task.CompletedTask;
|
|
//if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
//{
|
|
// //var bytes = QRCodeHelper.CaptureScreen(desktop);
|
|
// //await ViewModel?.ScanScreenResult(bytes);
|
|
//}
|
|
|
|
//ShowHideWindow(true);
|
|
}
|
|
|
|
private async Task ScanImageTaskAsync()
|
|
{
|
|
var fileName = await UI.OpenFileDialog(this, null);
|
|
if (fileName.IsNullOrEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ViewModel != null)
|
|
{
|
|
await ViewModel.ScanImageResult(fileName);
|
|
}
|
|
}
|
|
|
|
private void MenuCheckUpdate_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
_checkUpdateView ??= new CheckUpdateView();
|
|
DialogHost.Show(_checkUpdateView);
|
|
}
|
|
|
|
private void MenuBackupAndRestore_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
_backupAndRestoreView ??= new BackupAndRestoreView(this);
|
|
DialogHost.Show(_backupAndRestoreView);
|
|
}
|
|
|
|
private async void MenuClose_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (await UI.ShowYesNo(this, ResUI.menuExitTips) != ButtonResult.Yes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_blCloseByUser = true;
|
|
StorageUI();
|
|
|
|
await AppManager.Instance.AppExitAsync(true);
|
|
}
|
|
|
|
private void Shutdown(bool obj)
|
|
{
|
|
if (obj is bool b && _blCloseByUser == false)
|
|
{
|
|
_blCloseByUser = b;
|
|
}
|
|
StorageUI();
|
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
{
|
|
HotkeyManager.Instance.Dispose();
|
|
desktop.Shutdown();
|
|
}
|
|
}
|
|
|
|
#endregion Event
|
|
|
|
#region UI
|
|
|
|
public void ShowHideWindow(bool? blShow)
|
|
{
|
|
var bl = blShow ?? (!_config.UiItem.ShowInTaskbar ^ (WindowState == WindowState.Minimized));
|
|
if (bl)
|
|
{
|
|
this.Show();
|
|
if (this.WindowState == WindowState.Minimized)
|
|
{
|
|
this.WindowState = WindowState.Normal;
|
|
}
|
|
this.Activate();
|
|
this.Focus();
|
|
}
|
|
else
|
|
{
|
|
if (Utils.IsLinux() && _config.UiItem.Hide2TrayWhenClose == false)
|
|
{
|
|
this.WindowState = WindowState.Minimized;
|
|
return;
|
|
}
|
|
|
|
foreach (var ownedWindow in this.OwnedWindows)
|
|
{
|
|
ownedWindow.Close();
|
|
}
|
|
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
|
|
}
|