using Avalonia.Controls.DataGridDragDrop; using DialogHostAvalonia; using v2rayN.Desktop.Common; namespace v2rayN.Desktop.Views; public partial class ProfilesView : ReactiveUserControl { private static Config _config; private static readonly string _tag = "ProfilesView"; private bool _isDndRegistered = false; public ProfilesView() { InitializeComponent(); _config = AppManager.Instance.Config; menuSelectAll.Click += menuSelectAll_Click; btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; txtServerFilter.KeyDown += TxtServerFilter_KeyDown; lstProfiles.KeyDown += LstProfiles_KeyDown; lstProfiles.SelectionChanged += lstProfiles_SelectionChanged; lstProfiles.DoubleTapped += LstProfiles_DoubleTapped; lstProfiles.Sorting += LstProfiles_Sorting; //if (_config.UiItem.EnableDragDropSort) //{ // lstProfiles.SetValue(DragDrop.AllowDropProperty, true); // lstProfiles.AddHandler(PointerPressedEvent, LstProfiles_PointerPressed, RoutingStrategies.Bubble, true); // lstProfiles.AddHandler(PointerMovedEvent, LstProfiles_PointerMoved, RoutingStrategies.Bubble, true); // lstProfiles.AddHandler(PointerReleasedEvent, LstProfiles_PointerReleased, RoutingStrategies.Bubble, true); // lstProfiles.AddHandler(DragDrop.DragOverEvent, LstProfiles_DragOver, RoutingStrategies.Bubble); // lstProfiles.AddHandler(DragDrop.DropEvent, LstProfiles_Drop, RoutingStrategies.Bubble); //} this.WhenActivated(disposables => { this.OneWayBind(ViewModel, vm => vm.ProfileItems, v => v.lstProfiles.ItemsSource).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedProfile, v => v.lstProfiles.SelectedItem).DisposeWith(disposables); // this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.lstGroup.ItemsSource).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSub, v => v.lstGroup.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddSubCmd, v => v.btnAddSub).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.EditSubCmd, v => v.btnEditSub).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.EditSubCmd, v => v.menuSubEdit).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddSubCmd, v => v.menuSubAdd).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.DeleteSubCmd, v => v.menuSubDelete).DisposeWith(disposables); //servers delete this.BindCommand(ViewModel, vm => vm.EditServerCmd, v => v.menuEditServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RemoveServerCmd, v => v.menuRemoveServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RemoveDuplicateServerCmd, v => v.menuRemoveDuplicateServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupAllServerCmd, v => v.menuGenGroupAllServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupRegionServerCmd, v => v.menuGenGroupRegionServer).DisposeWith(disposables); //servers move //this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); //this.Bind(ViewModel, vm => vm.SelectedMoveToGroup, v => v.cmbMoveToGroup.SelectedItem).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables); //servers ping this.BindCommand(ViewModel, vm => vm.MixedTestServerCmd, v => v.menuMixedTestServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.TcpingServerCmd, v => v.menuTcpingServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RealPingServerCmd, v => v.menuRealPingServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.UdpTestServerCmd, v => v.menuUdpTestServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SpeedServerCmd, v => v.menuSpeedServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SortServerResultCmd, v => v.menuSortServerResult).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RemoveInvalidServerResultCmd, v => v.menuRemoveInvalidServerResult).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.FastRealPingCmd, v => v.btnFastRealPing).DisposeWith(disposables); //servers export this.BindCommand(ViewModel, vm => vm.Export2ClientConfigCmd, v => v.menuExport2ClientConfig).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2InnerUriCmd, v => v.menuExport2InnerUri).DisposeWith(disposables); if (_config.UiItem.EnableDragDropSort && !_isDndRegistered) { lstProfiles.CanUserReorderRows = true; lstProfiles.RowDragHandle = DataGridRowDragHandle.RowHeaderAndRow; lstProfiles.RowDragHandleVisible = true; var rowDragDropOptions = new DataGridRowDragDropOptions() { AllowedEffects = DragDropEffects.Move, DragSelectedRows = true, SuppressSelectionDragFromDragHandle = true, }; lstProfiles.RowDragDropOptions = rowDragDropOptions; lstProfiles.RowDropHandler = new ProfileRowDropHandler(ViewModel); _isDndRegistered = true; } ViewModel.ShowYesNoInteraction.RegisterHandler(async interaction => { var message = interaction.Input; var result = await UI.ShowYesNo(message); interaction.SetOutput(result == ButtonResult.Yes); }).DisposeWith(disposables); ViewModel.SaveFileDialogInteraction.RegisterHandler(async interaction => { var viewModel = ViewModel; if (viewModel is null) { interaction.SetOutput(false); return; } var profileItem = interaction.Input; var fileName = await UI.SaveFileDialog(""); if (fileName.IsNullOrEmpty()) { interaction.SetOutput(false); return; } await viewModel.Export2ClientConfigResult(fileName, profileItem); interaction.SetOutput(true); }).DisposeWith(disposables); ViewModel.SetClipboardDataInteraction.RegisterHandler(async interaction => { var strData = interaction.Input; await AvaUtils.SetClipboardData(this, strData); interaction.SetOutput(RxVoid.Default); }).DisposeWith(disposables); ViewModel.ProfilesFocusInteraction.RegisterHandler(interaction => { lstProfiles.Focus(); interaction.SetOutput(RxVoid.Default); }).DisposeWith(disposables); ViewModel.ShareServerInteraction.RegisterHandler(async interaction => { var url = interaction.Input; if (url.IsNullOrEmpty()) { interaction.SetOutput(RxVoid.Default); return; } await ShareServer(url); interaction.SetOutput(RxVoid.Default); }).DisposeWith(disposables); ViewModel.DispatcherRefreshServersBizInteraction.RegisterHandler(interaction => { Dispatcher.UIThread.Post(RefreshServersBiz, DispatcherPriority.Default); interaction.SetOutput(RxVoid.Default); }).DisposeWith(disposables); ViewModel.AdjustMainLvColWidthInteraction.RegisterHandler(interaction => { //AutofitColumnWidth(); interaction.SetOutput(RxVoid.Default); }).DisposeWith(disposables); AppEvents.AppExitRequested .AsObservable() .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(_ => StorageUI()) .DisposeWith(disposables); }); RestoreUI(); } private async void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e) { e.Handled = true; if (ViewModel != null && e.Column?.Tag?.ToString() != null) { await ViewModel.SortServer(e.Column.Tag.ToString()); } e.Handled = false; } #region Event public async Task ShareServer(string url) { if (url.IsNullOrEmpty()) { return; } var dialog = new QrcodeView(url); await DialogHost.Show(dialog); } public void RefreshServersBiz() { if (lstProfiles.SelectedIndex >= 0) { lstProfiles.ScrollIntoView(lstProfiles.SelectedItem, null); } } private void lstProfiles_SelectionChanged(object? sender, SelectionChangedEventArgs e) { if (ViewModel != null) { ViewModel.SelectedProfiles = lstProfiles.SelectedItems.Cast().ToList(); } } private void LstProfiles_DoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e) { var source = e.Source as Border; if (source?.Name == "HeaderBackground") { return; } if (_config.UiItem.DoubleClick2Activate) { ViewModel?.SetDefaultServer(); } else { ViewModel?.EditServerAsync(); } } //private void LstProfiles_ColumnHeader_Click(object? sender, RoutedEventArgs e) //{ // var colHeader = sender as DataGridColumnHeader; // if (colHeader == null || colHeader.TabIndex < 0 || colHeader.Column == null) // { // return; // } // var colName = ((MyDGTextColumn)colHeader.Column).ExName; // ViewModel?.SortServer(colName); //} private void menuSelectAll_Click(object? sender, RoutedEventArgs e) { lstProfiles.SelectAll(); } private void LstProfiles_KeyDown(object? sender, KeyEventArgs e) { if (e.KeyModifiers is KeyModifiers.Control or KeyModifiers.Meta) { switch (e.Key) { case Key.A: menuSelectAll_Click(null, null); break; case Key.C: ViewModel?.Export2ShareUrlAsync(false); break; case Key.D: ViewModel?.EditServerAsync(); break; case Key.F: ViewModel?.ShareServerAsync(); break; case Key.O: ViewModel?.ServerSpeedtest(ESpeedActionType.Tcping); break; case Key.R: ViewModel?.ServerSpeedtest(ESpeedActionType.Realping); break; case Key.T: ViewModel?.ServerSpeedtest(ESpeedActionType.Speedtest); break; case Key.E: ViewModel?.ServerSpeedtest(ESpeedActionType.Mixedtest); break; } } else { switch (e.Key) { case Key.Enter: //case Key.Return: ViewModel?.SetDefaultServer(); break; case Key.Delete: case Key.Back: ViewModel?.RemoveServerAsync(); break; case Key.T: ViewModel?.MoveServer(EMove.Top); break; case Key.U: ViewModel?.MoveServer(EMove.Up); break; case Key.D: ViewModel?.MoveServer(EMove.Down); break; case Key.B: ViewModel?.MoveServer(EMove.Bottom); break; case Key.Escape: ViewModel?.ServerSpeedtestStop(); break; } } } private void BtnAutofitColumnWidth_Click(object? sender, RoutedEventArgs e) { AutofitColumnWidth(); } private void AutofitColumnWidth() { 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); } } catch (Exception ex) { Logging.SaveLog(_tag, ex); } } private void TxtServerFilter_KeyDown(object? sender, KeyEventArgs e) { if (e.Key is Key.Enter or Key.Return) { ViewModel?.RefreshServers(); } } #endregion Event #region UI private void RestoreUI() { try { var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList(); var displayIndex = 0; foreach (var item in lvColumnItem) { foreach (var item2 in lstProfiles.Columns) { if (item2.Tag == null) { continue; } if (item2.Tag.Equals(item.Name)) { if (item.Width < 0) { item2.IsVisible = false; } else { item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel); item2.DisplayIndex = displayIndex++; } if (item.Name.StartsWith("to", StringComparison.CurrentCultureIgnoreCase)) { item2.IsVisible = _config.GuiItem.EnableStatistics; } if (item.Name.Equals("IpInfo", StringComparison.CurrentCultureIgnoreCase)) { item2.IsVisible = _config.SpeedTestItem.IPAPIUrl.IsNotEmpty() && !_config.UiItem.HideColumnIpInfo; } } } } } catch (Exception ex) { Logging.SaveLog(_tag, ex); } } private void StorageUI() { try { List lvColumnItem = []; foreach (var item2 in lstProfiles.Columns) { if (item2.Tag == null) { continue; } lvColumnItem.Add(new() { Name = (string)item2.Tag, Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1), Index = item2.DisplayIndex }); } _config.UiItem.MainColumnItem = lvColumnItem; } catch (Exception ex) { Logging.SaveLog(_tag, ex); } } #endregion UI //#region Drag and Drop //private static readonly DataFormat LstProfilesRowFormat = // DataFormat.CreateInProcessFormat("LstProfilesRow"); //private (Point, PointerPressedEventArgs)? _dragStartPoint; //private void LstProfiles_PointerPressed(object? sender, PointerPressedEventArgs e) //{ // var properties = e.GetCurrentPoint(this).Properties; // if (properties.IsLeftButtonPressed) // { // _dragStartPoint = (e.GetPosition(this), e); // } //} //private async void LstProfiles_PointerMoved(object? sender, PointerEventArgs e) //{ // try // { // if (_dragStartPoint == null) // { // return; // } // var properties = e.GetCurrentPoint(this).Properties; // if (!properties.IsLeftButtonPressed) // { // _dragStartPoint = null; // return; // } // var currentPoint = e.GetPosition(this); // var startPoint = _dragStartPoint.Value.Item1; // var delta = startPoint - currentPoint; // var threshold = new Vector(4, 4); // if (!(Math.Abs(delta.X) >= threshold.X) && !(Math.Abs(delta.Y) >= threshold.Y)) // { // return; // } // var dragStartEventArgs = _dragStartPoint.Value.Item2; // _dragStartPoint = null; // if (e.Source is not Visual visualSource) // { // return; // } // var row = visualSource.FindAncestorOfType(true); // if (row?.DataContext == null) // { // return; // } // e.Handled = true; // var dragData = new DataTransfer(); // var item = DataTransferItem.Create(LstProfilesRowFormat, row.DataContext as ProfileItemModel); // dragData.Add(item); // await DragDrop.DoDragDropAsync(dragStartEventArgs, dragData, DragDropEffects.Move); // } // catch // { // // Ignore // } //} //private void LstProfiles_PointerReleased(object? sender, PointerReleasedEventArgs e) //{ // _dragStartPoint = null; //} //private void LstProfiles_DragOver(object? sender, DragEventArgs e) //{ // if (!e.DataTransfer.Contains(LstProfilesRowFormat)) // { // e.DragEffects = DragDropEffects.None; // return; // } // e.DragEffects = DragDropEffects.Move; //} //private void LstProfiles_Drop(object? sender, DragEventArgs e) //{ // if (!e.DataTransfer.Contains(LstProfilesRowFormat)) // { // return; // } // ProfileItemModel? sourceItem = null; // foreach (var item in e.DataTransfer.Items) // { // if (!item.Formats.Contains(LstProfilesRowFormat)) // { // continue; // } // if (item.TryGetRaw(LstProfilesRowFormat) is not ProfileItemModel model) // { // continue; // } // sourceItem = model; // break; // } // if (sourceItem == null) // { // return; // } // if (e.Source is not Visual visualTarget) // { // return; // } // var targetRow = visualTarget.FindAncestorOfType(true); // if (targetRow is not { DataContext: ProfileItemModel targetItem }) // { // return; // } // if (ReferenceEquals(sourceItem, targetItem)) // { // return; // } // if (lstProfiles.ItemsSource is not IList items) // { // return; // } // var oldIndex = items.IndexOf(sourceItem); // var newIndex = items.IndexOf(targetItem); // if (oldIndex >= 0 && newIndex >= 0) // { // ViewModel?.MoveServerTo(oldIndex, targetItem); // } //} //#endregion Drag and Drop private sealed class ProfileRowDropHandler(ProfilesViewModel viewModel) : IDataGridRowDropHandler { private readonly DataGridRowReorderHandler _reorderHandler = new(); public bool Validate(DataGridRowDropEventArgs args) { ArgumentNullException.ThrowIfNull(args); return _reorderHandler.Validate(args); } public bool Execute(DataGridRowDropEventArgs args) { ArgumentNullException.ThrowIfNull(args); if (!_reorderHandler.Execute(args)) { return false; } if (viewModel?.ProfileItems != null) { for (var index = 0; index < viewModel.ProfileItems.Count; index++) { var item = viewModel.ProfileItems[index]; var sort = (index + 1) * 10; item.Sort = sort; ProfileExManager.Instance.SetSort(item.IndexId, sort); } } return true; } } }