Files
v2rayN/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
T
DHR60 66546700b0 Fix UI (#10527)
* Revert "Cache selected views in SimpleViewLocator"

This reverts commit 8ebbd7f4f8.

* Fix UI

* Optimize Styles

* Code clean
2026-11-05 10:08:04 +08:00

606 lines
24 KiB
C#

using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using DialogHostAvalonia;
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;
private MsgView? _msgView;
private ProfilesView? _profilesView;
private ClashConnectionsView? _clashConnectionsView;
private ClashProxiesView? _clashProxiesView;
public MainWindow()
{
InitializeComponent();
_config = AppManager.Instance.Config;
_manager = new WindowNotificationManager(TopLevel.GetTopLevel(this)) { MaxItems = 3, Position = NotificationPosition.TopRight };
KeyDown += MainWindow_KeyDown;
menuSettingsSetUWP.Click += MenuSettingsSetUWP_Click;
menuPromotion.Click += MenuPromotion_Click;
menuCheckUpdate.Click += MenuCheckUpdate_Click;
btnNewUpdate.Click += MenuCheckUpdate_Click;
menuBackupAndRestore.Click += MenuBackupAndRestore_Click;
menuClose.Click += MenuClose_Click;
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.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddMasqueServerCmd, v => v.menuAddMasqueServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomOutboundServerCmd, v => v.menuAddCustomOutboundServer).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);
this.OneWayBind(ViewModel, vm => vm.BlNewUpdate, v => v.btnNewUpdate.IsVisible).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.StatusBarViewModel, v => v.contentStatusBarView.Content).DisposeWith(disposables);
this.WhenAnyValue(v => v.ViewModel.MainGirdOrientation)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(orientation =>
{
UpdateLayout(orientation, disposables);
})
.DisposeWith(disposables);
ViewModel.ReadTextFromClipboardInteraction.RegisterHandler(async interaction =>
{
var result = await AvaUtils.GetClipboardData(this);
interaction.SetOutput(result);
}).DisposeWith(disposables);
ViewModel.ScanScreenInteraction.RegisterHandler(async interaction =>
{
ShowHideWindow(false);
await Task.Delay(200);
var result = QRCodeAvaloniaUtils.CaptureScreen();
ShowHideWindow(true);
interaction.SetOutput(result);
}).DisposeWith(disposables);
ViewModel.BrowseImageFileInteraction.RegisterHandler(async interaction =>
{
var result = await UI.OpenFileDialog(null);
interaction.SetOutput(result);
}).DisposeWith(disposables);
ViewModel.ShowHideWindowInteraction.RegisterHandler(interaction =>
{
ShowHideWindow(interaction.Input);
interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables);
AppEvents.SendSnackMsgRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async content => await DelegateSnackMsg(content))
.DisposeWith(disposables);
AppEvents.AppExitRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(_ => StorageUI())
.DisposeWith(disposables);
AppEvents.ShutdownRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(Shutdown)
.DisposeWith(disposables);
});
if (Utils.IsWindows())
{
Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}";
if (!Design.IsDesignMode)
{
ThreadPool.RegisterWaitForSingleObject(Program.ProgramStarted, OnProgramStarted, null, -1, false);
HotkeyManager.Instance.Init(_config, OnHotkeyHandler);
}
}
else
{
Title = $"{Utils.GetVersion()}";
menuAddServerViaScan.IsVisible = false;
}
if (_config.UiItem.AutoHideStartup && Utils.IsWindows())
{
WindowState = WindowState.Minimized;
}
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 Avalonia.Controls.Notifications.Notification(null, content, NotificationType.Information));
await Task.CompletedTask;
}
private void OnHotkeyHandler(EGlobalHotkey e)
{
switch (e)
{
case EGlobalHotkey.ShowForm:
Dispatcher.UIThread.Post(() => 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);
await Task.Delay(200);
var bytes = QRCodeAvaloniaUtils.CaptureScreen();
if (bytes != null && ViewModel != null)
{
await ViewModel.ScanScreenResult(bytes);
}
ShowHideWindow(true);
}
private void MenuCheckUpdate_Click(object? sender, RoutedEventArgs e)
{
_checkUpdateView ??= new CheckUpdateView();
_checkUpdateView.ViewModel = ViewModel?.CheckUpdateViewModel;
DialogHost.Show(_checkUpdateView);
AppEvents.HasUpdateNotified.Publish(false);
}
private void MenuBackupAndRestore_Click(object? sender, RoutedEventArgs e)
{
_backupAndRestoreView ??= new BackupAndRestoreView();
_backupAndRestoreView.ViewModel = ViewModel?.BackupAndRestoreViewModel;
DialogHost.Show(_backupAndRestoreView);
}
private async void MenuClose_Click(object? sender, RoutedEventArgs e)
{
try
{
if (await UI.ShowYesNo(ResUI.menuExitTips) != ButtonResult.Yes)
{
return;
}
_blCloseByUser = true;
StorageUI();
await AppManager.Instance.AppExitAsync(true);
}
catch
{
// Ignore
}
}
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 ??
(Utils.IsLinux() || Utils.IsMacOS()
? (!AppManager.Instance.ShowInTaskbar ^ (WindowState == WindowState.Minimized))
: !AppManager.Instance.ShowInTaskbar);
if (bl)
{
Show();
if (WindowState == WindowState.Minimized)
{
WindowState = WindowState.Normal;
}
Activate();
Focus();
}
else
{
if (Utils.IsLinux() && _config.UiItem.Hide2TrayWhenClose == false)
{
WindowState = WindowState.Minimized;
return;
}
foreach (var ownedWindow in OwnedWindows)
{
ownedWindow.Close();
}
Hide();
}
AppManager.Instance.ShowInTaskbar = bl;
}
protected override void OnLoaded(object? sender, RoutedEventArgs e)
{
base.OnLoaded(sender, e);
if (_config.UiItem.AutoHideStartup)
{
ShowHideWindow(false);
}
}
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)
{
gridMain.RowDefinitions[0].Height = new GridLength(_config.UiItem.MainGirdHeight1, GridUnitType.Star);
gridMain.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, gridMain.RowDefinitions[0].ActualHeight, gridMain.RowDefinitions[2].ActualHeight);
}
}
private void UpdateLayout(EGirdOrientation orientation, MultipleDisposable disposable)
{
gridMain.IsVisible = true;
_msgView ??= new MsgView
{
DataContext = ViewModel?.MsgViewModel,
};
_profilesView ??= new ProfilesView
{
DataContext = ViewModel?.ProfilesViewModel,
};
_clashProxiesView ??= new ClashProxiesView
{
DataContext = ViewModel?.ClashProxiesViewModel,
};
_clashConnectionsView ??= new ClashConnectionsView
{
DataContext = ViewModel?.ClashConnectionsViewModel,
};
switch (orientation)
{
case EGirdOrientation.Horizontal:
default:
gridMain.ColumnDefinitions = new ColumnDefinitions("1*,10,1*");
gridMain.RowDefinitions = new RowDefinitions("1*");
Grid.SetColumn(tabProfiles, 0);
Grid.SetColumn(gridSplitter, 1);
Grid.SetColumn(tabDockPanel, 2);
Grid.SetRow(tabProfiles, 0);
Grid.SetRow(gridSplitter, 0);
Grid.SetRow(tabDockPanel, 0);
DockPanel.SetDock(tabStrip, Dock.Top);
tabProfiles.Content = _profilesView;
gridSplitter.HorizontalAlignment = HorizontalAlignment.Stretch;
tabStrip.ItemsPanel = new FuncTemplate<Panel?>(() => new StackPanel
{
Orientation = Orientation.Horizontal,
});
this.WhenAnyValue(x => x.ViewModel!.ShowClashUI)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(show =>
{
if (show)
{
tabStrip.Items.Clear();
tabStrip.Items.Add(new TabStripItem { Content = ResUI.MsgInformationTitle });
tabStrip.Items.Add(new TabStripItem { Content = ResUI.TbProxies });
tabStrip.Items.Add(new TabStripItem { Content = ResUI.TbConnections });
tabStrip.IsVisible = true;
}
else
{
tabStrip.IsVisible = false;
tabStrip.Items.Clear();
}
}).DisposeWith(disposable);
tabStrip.GetObservable(SelectingItemsControl.SelectedIndexProperty)
.Subscribe(index =>
{
tabContent.Content = index switch
{
1 => _clashProxiesView,
2 => _clashConnectionsView,
_ => _msgView,
};
}).DisposeWith(disposable);
break;
case EGirdOrientation.Vertical:
gridMain.RowDefinitions = new RowDefinitions("1*,10,1*");
gridMain.ColumnDefinitions = new ColumnDefinitions("1*");
Grid.SetRow(tabProfiles, 0);
Grid.SetRow(gridSplitter, 1);
Grid.SetRow(tabDockPanel, 2);
Grid.SetColumn(tabProfiles, 0);
Grid.SetColumn(gridSplitter, 0);
Grid.SetColumn(tabDockPanel, 0);
DockPanel.SetDock(tabStrip, Dock.Left);
tabProfiles.Content = _profilesView;
gridSplitter.VerticalAlignment = VerticalAlignment.Stretch;
tabStrip.ItemsPanel = new FuncTemplate<Panel?>(() => new StackPanel
{
Orientation = Orientation.Vertical,
});
this.WhenAnyValue(x => x.ViewModel!.ShowClashUI)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(show =>
{
if (show)
{
tabStrip.Items.Clear();
tabStrip.Items.Add(new TabStripItem { Content = ResUI.MsgInformationTitle });
tabStrip.Items.Add(new TabStripItem { Content = ResUI.TbProxies });
tabStrip.Items.Add(new TabStripItem { Content = ResUI.TbConnections });
tabStrip.IsVisible = true;
}
else
{
tabStrip.IsVisible = false;
tabStrip.Items.Clear();
}
}).DisposeWith(disposable);
tabStrip.GetObservable(SelectingItemsControl.SelectedIndexProperty)
.Subscribe(index =>
{
tabContent.Content = index switch
{
1 => _clashProxiesView,
2 => _clashConnectionsView,
_ => _msgView,
};
}).DisposeWith(disposable);
break;
case EGirdOrientation.Tab:
gridMain.RowDefinitions = new RowDefinitions("1*");
gridMain.ColumnDefinitions = new ColumnDefinitions("1*");
Grid.SetColumn(tabProfiles, 0);
Grid.SetColumn(gridSplitter, 0);
Grid.SetColumn(tabDockPanel, 0);
Grid.SetRow(tabProfiles, 0);
Grid.SetRow(gridSplitter, 0);
Grid.SetRow(tabDockPanel, 0);
DockPanel.SetDock(tabStrip, Dock.Left);
tabProfiles.Content = null;
gridSplitter.VerticalAlignment = VerticalAlignment.Stretch;
tabStrip.ItemsPanel = new FuncTemplate<Panel?>(() => new StackPanel
{
Orientation = Orientation.Vertical,
});
this.WhenAnyValue(x => x.ViewModel!.ShowClashUI)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(show =>
{
if (show)
{
tabStrip.Items.Clear();
tabStrip.Items.Add(new TabStripItem { Content = ResUI.menuServers });
tabStrip.Items.Add(new TabStripItem { Content = ResUI.MsgInformationTitle });
tabStrip.Items.Add(new TabStripItem { Content = ResUI.TbProxies });
tabStrip.Items.Add(new TabStripItem { Content = ResUI.TbConnections });
tabStrip.IsVisible = true;
}
else
{
tabStrip.IsVisible = false;
tabStrip.Items.Clear();
}
}).DisposeWith(disposable);
tabStrip.GetObservable(SelectingItemsControl.SelectedIndexProperty)
.Subscribe(index =>
{
tabContent.Content = index switch
{
1 => _msgView,
2 => _clashProxiesView,
3 => _clashConnectionsView,
_ => _profilesView,
};
}).DisposeWith(disposable);
break;
}
RestoreUI();
}
private void AddHelpMenuItem()
{
var coreInfo = CoreInfoManager.Instance.GetCoreInfo();
foreach (var it in coreInfo
.Where(t => t.CoreType is not ECoreType.v2fly
and not 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
}