From 04195c29573525151d85b2a9b2d9761c23df4f3d Mon Sep 17 00:00:00 2001 From: DHR60 Date: Sun, 7 Sep 2025 18:58:59 +0800 Subject: [PATCH] Profiles Select Window (#7891) * Profiles Select Window * Sort * wpf * avalonia * Allow single select * Fix * Add Config Type Filter * Remove unnecessary --- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 9 + v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.hu.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.ru.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 3 + .../ViewModels/ProfilesSelectViewModel.cs | 359 ++++++++++++++++++ .../ViewModels/ProfilesViewModel.cs | 25 -- .../Views/ProfilesSelectWindow.axaml | 136 +++++++ .../Views/ProfilesSelectWindow.axaml.cs | 200 ++++++++++ .../Views/RoutingRuleDetailsWindow.axaml | 15 +- .../Views/RoutingRuleDetailsWindow.axaml.cs | 16 + .../v2rayN.Desktop/Views/SubEditWindow.axaml | 12 + .../Views/SubEditWindow.axaml.cs | 31 ++ v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml | 156 ++++++++ .../v2rayN/Views/ProfilesSelectWindow.xaml.cs | 194 ++++++++++ v2rayN/v2rayN/Views/ProfilesView.xaml.cs | 4 +- .../Views/RoutingRuleDetailsWindow.xaml | 16 +- .../Views/RoutingRuleDetailsWindow.xaml.cs | 15 + v2rayN/v2rayN/Views/SubEditWindow.xaml | 16 + v2rayN/v2rayN/Views/SubEditWindow.xaml.cs | 28 ++ 22 files changed, 1217 insertions(+), 33 deletions(-) create mode 100644 v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs create mode 100644 v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml create mode 100644 v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs create mode 100644 v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml create mode 100644 v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 82d1203f..6503b41c 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -3030,6 +3030,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Select Profile 的本地化字符串。 + /// + public static string TbSelectProfile { + get { + return ResourceManager.GetString("TbSelectProfile", resourceCulture); + } + } + /// /// 查找类似 Set system proxy 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 302c91c4..0c8ca393 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1512,4 +1512,7 @@ Start parsing and processing subscription content + + Select Profile + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 8f6079fd..800fa00f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1512,4 +1512,7 @@ Start parsing and processing subscription content + + Select Profile + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 05bcf901..614c8092 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1512,4 +1512,7 @@ Start parsing and processing subscription content + + Select Profile + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index fd2f5400..294d9f34 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1512,4 +1512,7 @@ Start parsing and processing subscription content + + Select Profile + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 7176222a..194a59e2 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1509,4 +1509,7 @@ 开始解析和处理订阅内容 + + 选择配置文件 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 58239c8c..fa84c789 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1509,4 +1509,7 @@ 開始解析和處理訂閱內容 + + Select Profile + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs new file mode 100644 index 00000000..ff6d787f --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; + +namespace ServiceLib.ViewModels; +public class ProfilesSelectViewModel : MyReactiveObject +{ + #region private prop + + private List _lstProfile; + private string _serverFilter = string.Empty; + private Dictionary _dicHeaderSort = new(); + private string _subIndexId = string.Empty; + // ConfigType filter state: default include-mode with all types selected + private List _filterConfigTypes = new(); + private bool _filterExclude = false; + + #endregion private prop + + #region ObservableCollection + + public IObservableCollection ProfileItems { get; } = new ObservableCollectionExtended(); + + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); + + [Reactive] + public ProfileItemModel SelectedProfile { get; set; } + + public IList SelectedProfiles { get; set; } + + [Reactive] + public SubItem SelectedSub { get; set; } + + [Reactive] + public string ServerFilter { get; set; } + + // Include/Exclude filter for ConfigType + public List FilterConfigTypes + { + get => _filterConfigTypes; + set => this.RaiseAndSetIfChanged(ref _filterConfigTypes, value); + } + + [Reactive] + public bool FilterExclude + { + get => _filterExclude; + set => this.RaiseAndSetIfChanged(ref _filterExclude, value); + } + + #endregion ObservableCollection + + #region Init + + public ProfilesSelectViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + _subIndexId = _config.SubIndexId ?? string.Empty; + #region WhenAnyValue && ReactiveCommand + + this.WhenAnyValue( + x => x.SelectedSub, + y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id) + .Subscribe(async c => await SubSelectedChangedAsync(c)); + + this.WhenAnyValue( + x => x.ServerFilter, + y => y != null && _serverFilter != y) + .Subscribe(async c => await ServerFilterChanged(c)); + + // React to ConfigType filter changes + this.WhenAnyValue(x => x.FilterExclude) + .Skip(1) + .Subscribe(async _ => await RefreshServersBiz()); + + this.WhenAnyValue(x => x.FilterConfigTypes) + .Skip(1) + .Subscribe(async _ => await RefreshServersBiz()); + + #endregion WhenAnyValue && ReactiveCommand + + _ = Init(); + } + + private async Task Init() + { + SelectedProfile = new(); + SelectedSub = new(); + + // Default: include mode with all ConfigTypes selected + try + { + FilterExclude = false; + FilterConfigTypes = Enum.GetValues(typeof(EConfigType)).Cast().ToList(); + } + catch + { + FilterConfigTypes = new(); + } + + await RefreshSubscriptions(); + await RefreshServers(); + } + + #endregion Init + + #region Actions + + public bool CanOk() + { + return SelectedProfile != null && !SelectedProfile.IndexId.IsNullOrEmpty(); + } + + public bool SelectFinish() + { + if (!CanOk()) + { + return false; + } + _updateView?.Invoke(EViewAction.CloseWindow, null); + return true; + } + #endregion Actions + + #region Servers && Groups + + private async Task SubSelectedChangedAsync(bool c) + { + if (!c) + { + return; + } + _subIndexId = SelectedSub?.Id; + + await RefreshServers(); + + await _updateView?.Invoke(EViewAction.ProfilesFocus, null); + } + + private async Task ServerFilterChanged(bool c) + { + if (!c) + { + return; + } + _serverFilter = ServerFilter; + if (_serverFilter.IsNullOrEmpty()) + { + await RefreshServers(); + } + } + + public async Task RefreshServers() + { + await RefreshServersBiz(); + } + + private async Task RefreshServersBiz() + { + var lstModel = await GetProfileItemsEx(_subIndexId, _serverFilter); + _lstProfile = JsonUtils.Deserialize>(JsonUtils.Serialize(lstModel)) ?? []; + + ProfileItems.Clear(); + ProfileItems.AddRange(lstModel); + if (lstModel.Count > 0) + { + var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); + if (selected != null) + { + SelectedProfile = selected; + } + else + { + SelectedProfile = lstModel.First(); + } + } + + await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); + } + + public async Task RefreshSubscriptions() + { + SubItems.Clear(); + + SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers }); + + foreach (var item in await AppManager.Instance.SubItems()) + { + SubItems.Add(item); + } + if (_subIndexId != null && SubItems.FirstOrDefault(t => t.Id == _subIndexId) != null) + { + SelectedSub = SubItems.FirstOrDefault(t => t.Id == _subIndexId); + } + else + { + SelectedSub = SubItems.First(); + } + } + + private async Task?> GetProfileItemsEx(string subid, string filter) + { + var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter); + + //await ConfigHandler.SetDefaultServer(_config, lstModel); + + lstModel = (from t in lstModel + select new ProfileItemModel + { + IndexId = t.IndexId, + ConfigType = t.ConfigType, + Remarks = t.Remarks, + Address = t.Address, + Port = t.Port, + Security = t.Security, + Network = t.Network, + StreamSecurity = t.StreamSecurity, + Subid = t.Subid, + SubRemarks = t.SubRemarks, + IsActive = t.IndexId == _config.IndexId, + }).OrderBy(t => t.Sort).ToList(); + + // Apply ConfigType filter (include or exclude) + if (FilterConfigTypes != null && FilterConfigTypes.Count > 0) + { + if (FilterExclude) + { + lstModel = lstModel.Where(t => !FilterConfigTypes.Contains(t.ConfigType)).ToList(); + } + else + { + lstModel = lstModel.Where(t => FilterConfigTypes.Contains(t.ConfigType)).ToList(); + } + } + + return lstModel; + } + + public async Task GetProfileItem() + { + if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) + { + return null; + } + var indexId = SelectedProfile.IndexId; + var item = await AppManager.Instance.GetProfileItem(indexId); + if (item is null) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return null; + } + return item; + } + + public async Task?> GetProfileItems() + { + if (SelectedProfiles == null || SelectedProfiles.Count == 0) + { + return null; + } + var lst = new List(); + foreach (var sp in SelectedProfiles) + { + if (string.IsNullOrEmpty(sp?.IndexId)) + { + continue; + } + var item = await AppManager.Instance.GetProfileItem(sp.IndexId); + if (item != null) + { + lst.Add(item); + } + } + if (lst.Count == 0) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return null; + } + return lst; + } + + public void SortServer(string colName) + { + if (colName.IsNullOrEmpty()) + { + return; + } + + var prop = typeof(ProfileItemModel).GetProperty(colName); + if (prop == null) + { + return; + } + + _dicHeaderSort.TryAdd(colName, true); + var asc = _dicHeaderSort[colName]; + + var comparer = Comparer.Create((a, b) => + { + if (ReferenceEquals(a, b)) + { + return 0; + } + if (a is null) + { + return -1; + } + if (b is null) + { + return 1; + } + if (a.GetType() == b.GetType() && a is IComparable ca) + { + return ca.CompareTo(b); + } + return string.Compare(a.ToString(), b.ToString(), StringComparison.OrdinalIgnoreCase); + }); + + object? KeySelector(ProfileItemModel x) + { + return prop.GetValue(x); + } + + IEnumerable sorted = asc + ? ProfileItems.OrderBy(KeySelector, comparer) + : ProfileItems.OrderByDescending(KeySelector, comparer); + + var list = sorted.ToList(); + ProfileItems.Clear(); + ProfileItems.AddRange(list); + + _dicHeaderSort[colName] = !asc; + + return; + } + + #endregion Servers && Groups + + #region Public API + + // External setter for ConfigType filter + public void SetConfigTypeFilter(IEnumerable types, bool exclude = false) + { + FilterConfigTypes = types?.Distinct().ToList() ?? new List(); + FilterExclude = exclude; + } + + #endregion Public API +} diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 5209f4dd..f5aeac21 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -38,15 +38,9 @@ public class ProfilesViewModel : MyReactiveObject [Reactive] public SubItem SelectedMoveToGroup { get; set; } - [Reactive] - public ComboItem SelectedServer { get; set; } - [Reactive] public string ServerFilter { get; set; } - [Reactive] - public bool BlServers { get; set; } - #endregion ObservableCollection #region Menu @@ -115,11 +109,6 @@ public class ProfilesViewModel : MyReactiveObject y => y != null && !y.Remarks.IsNullOrEmpty()) .Subscribe(async c => await MoveToGroup(c)); - this.WhenAnyValue( - x => x.SelectedServer, - y => y != null && !y.Text.IsNullOrEmpty()) - .Subscribe(async c => await ServerSelectedChanged(c)); - this.WhenAnyValue( x => x.ServerFilter, y => y != null && _serverFilter != y) @@ -266,7 +255,6 @@ public class ProfilesViewModel : MyReactiveObject SelectedProfile = new(); SelectedSub = new(); SelectedMoveToGroup = new(); - SelectedServer = new(); await RefreshSubscriptions(); await RefreshServers(); @@ -613,19 +601,6 @@ public class ProfilesViewModel : MyReactiveObject } } - private async Task ServerSelectedChanged(bool c) - { - if (!c) - { - return; - } - if (SelectedServer == null || SelectedServer.ID.IsNullOrEmpty()) - { - return; - } - await SetDefaultServer(SelectedServer.ID); - } - public async Task ShareServerAsync() { var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml new file mode 100644 index 00000000..abe98176 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs new file mode 100644 index 00000000..5df0251a --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs @@ -0,0 +1,200 @@ +using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; +using Avalonia.ReactiveUI; +using ReactiveUI; +using ServiceLib.Manager; +using v2rayN.Desktop.Common; + +namespace v2rayN.Desktop.Views; + +public partial class ProfilesSelectWindow : ReactiveWindow +{ + private static Config _config; + + public Task ProfileItem => GetProfileItem(); + public Task?> ProfileItems => GetProfileItems(); + private bool _allowMultiSelect = false; + + public ProfilesSelectWindow() + { + InitializeComponent(); + + _config = AppManager.Instance.Config; + + btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; + txtServerFilter.KeyDown += TxtServerFilter_KeyDown; + lstProfiles.KeyDown += LstProfiles_KeyDown; + lstProfiles.SelectionChanged += LstProfiles_SelectionChanged; + lstProfiles.LoadingRow += LstProfiles_LoadingRow; + lstProfiles.Sorting += LstProfiles_Sorting; + lstProfiles.DoubleTapped += LstProfiles_DoubleTapped; + + ViewModel = new ProfilesSelectViewModel(UpdateViewHandler); + DataContext = ViewModel; + + 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.Bind(ViewModel, vm => vm.SelectedSub, v => v.lstGroup.SelectedItem).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables); + }); + + btnCancel.Click += (s, e) => Close(false); + } + + public void AllowMultiSelect(bool allow) + { + _allowMultiSelect = allow; + if (allow) + { + lstProfiles.SelectionMode = DataGridSelectionMode.Extended; + lstProfiles.SelectedItems.Clear(); + } + else + { + lstProfiles.SelectionMode = DataGridSelectionMode.Single; + if (lstProfiles.SelectedItems.Count > 0) + { + var first = lstProfiles.SelectedItems[0]; + lstProfiles.SelectedItems.Clear(); + lstProfiles.SelectedItem = first; + } + } + } + + // Expose ConfigType filter controls to callers + public void SetConfigTypeFilter(IEnumerable types, bool exclude = false) + => ViewModel?.SetConfigTypeFilter(types, exclude); + + private async Task UpdateViewHandler(EViewAction action, object? obj) + { + switch (action) + { + case EViewAction.CloseWindow: + Close(true); + break; + } + return await Task.FromResult(true); + } + + private void LstProfiles_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (ViewModel != null) + { + ViewModel.SelectedProfiles = lstProfiles.SelectedItems.Cast().ToList(); + } + } + + private void LstProfiles_LoadingRow(object? sender, DataGridRowEventArgs e) + { + e.Row.Header = $" {e.Row.Index + 1}"; + } + + private void LstProfiles_DoubleTapped(object? sender, TappedEventArgs e) + { + // 忽略表头区域的双击 + if (e.Source is Control src) + { + if (src.FindAncestorOfType() != null) + { + e.Handled = true; + return; + } + + // 仅当在数据行或其子元素上双击时才触发选择 + if (src.FindAncestorOfType() != null) + { + ViewModel?.SelectFinish(); + e.Handled = true; + } + } + } + + private void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e) + { + // 自定义排序,防止默认行为导致误触发 + e.Handled = true; + if (ViewModel != null && e.Column?.Tag?.ToString() != null) + { + ViewModel.SortServer(e.Column.Tag.ToString()); + } + } + + private void LstProfiles_KeyDown(object? sender, KeyEventArgs e) + { + if (e.KeyModifiers is KeyModifiers.Control or KeyModifiers.Meta) + { + if (e.Key == Key.A) + { + if (_allowMultiSelect) + { + lstProfiles.SelectAll(); + } + e.Handled = true; + } + } + else + { + if (e.Key is Key.Enter or Key.Return) + { + ViewModel?.SelectFinish(); + e.Handled = true; + } + } + } + + private void BtnAutofitColumnWidth_Click(object? sender, RoutedEventArgs e) + { + AutofitColumnWidth(); + } + + private void AutofitColumnWidth() + { + try + { + foreach (var col in lstProfiles.Columns) + { + col.Width = new DataGridLength(1, DataGridLengthUnitType.Auto); + } + } + catch + { + } + } + + private void TxtServerFilter_KeyDown(object? sender, KeyEventArgs e) + { + if (e.Key is Key.Enter or Key.Return) + { + ViewModel?.RefreshServers(); + } + } + + public async Task GetProfileItem() + { + var item = await ViewModel?.GetProfileItem(); + return item; + } + + public async Task?> GetProfileItems() + { + var item = await ViewModel?.GetProfileItems(); + return item; + } + + private void BtnSave_Click(object sender, RoutedEventArgs e) + { + // Trigger selection finalize when Confirm is clicked + ViewModel?.SelectFinish(); + } +} diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml b/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml index f310f260..872a11f8 100644 --- a/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml @@ -54,13 +54,22 @@ Width="300" Margin="{StaticResource Margin4}" Text="{Binding SelectedSource.OutboundTag, Mode=TwoWay}" /> - + VerticalAlignment="Center"> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs b/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs new file mode 100644 index 00000000..e9e33ec5 --- /dev/null +++ b/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs @@ -0,0 +1,194 @@ +using System.Reactive.Disposables; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Threading; +using ReactiveUI; +using ServiceLib.Manager; +using Splat; +using v2rayN.Base; + +namespace v2rayN.Views; + +public partial class ProfilesSelectWindow +{ + private static Config _config; + + public Task ProfileItem => GetProfileItem(); + public Task?> ProfileItems => GetProfileItems(); + private bool _allowMultiSelect = false; + + public ProfilesSelectWindow() + { + InitializeComponent(); + lstGroup.MaxHeight = Math.Floor(SystemParameters.WorkArea.Height * 0.20 / 40) * 40; + + _config = AppManager.Instance.Config; + + btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; + txtServerFilter.PreviewKeyDown += TxtServerFilter_PreviewKeyDown; + lstProfiles.PreviewKeyDown += LstProfiles_PreviewKeyDown; + lstProfiles.SelectionChanged += LstProfiles_SelectionChanged; + lstProfiles.LoadingRow += LstProfiles_LoadingRow; + + ViewModel = new ProfilesSelectViewModel(UpdateViewHandler); + + + 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); + }); + } + + public void AllowMultiSelect(bool allow) + { + _allowMultiSelect = allow; + if (allow) + { + lstProfiles.SelectionMode = DataGridSelectionMode.Extended; + lstProfiles.SelectedItems.Clear(); + } + else + { + lstProfiles.SelectionMode = DataGridSelectionMode.Single; + if (lstProfiles.SelectedItems.Count > 0) + { + var first = lstProfiles.SelectedItems[0]; + lstProfiles.SelectedItems.Clear(); + lstProfiles.SelectedItem = first; + } + } + } + + // Expose ConfigType filter controls to callers + public void SetConfigTypeFilter(IEnumerable types, bool exclude = false) + => ViewModel?.SetConfigTypeFilter(types, exclude); + + #region Event + + private async Task UpdateViewHandler(EViewAction action, object? obj) + { + switch (action) + { + case EViewAction.CloseWindow: + this.DialogResult = true; + break; + } + return await Task.FromResult(true); + } + + private void LstProfiles_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) + { + if (ViewModel != null) + { + ViewModel.SelectedProfiles = lstProfiles.SelectedItems.Cast().ToList(); + } + } + + private void LstProfiles_LoadingRow(object? sender, DataGridRowEventArgs e) + { + e.Row.Header = $" {e.Row.GetIndex() + 1}"; + } + + private void LstProfiles_MouseDoubleClick(object sender, MouseButtonEventArgs e) + { + ViewModel?.SelectFinish(); + } + + 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) + { + if (!_allowMultiSelect) + { + return; + } + lstProfiles.SelectAll(); + } + + private void LstProfiles_PreviewKeyDown(object sender, KeyEventArgs e) + { + if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) + { + switch (e.Key) + { + case Key.A: + menuSelectAll_Click(null, null); + e.Handled = true; + break; + } + } + else + { + if (e.Key is Key.Enter or Key.Return) + { + ViewModel?.SelectFinish(); + e.Handled = true; + } + } + } + + private void BtnAutofitColumnWidth_Click(object sender, RoutedEventArgs e) + { + AutofitColumnWidth(); + } + + private void AutofitColumnWidth() + { + try + { + foreach (var it in lstProfiles.Columns) + { + it.Width = new DataGridLength(1, DataGridLengthUnitType.Auto); + } + } + catch (Exception ex) + { + Logging.SaveLog("ProfilesView", ex); + } + } + + private void TxtServerFilter_PreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key is Key.Enter or Key.Return) + { + ViewModel?.RefreshServers(); + e.Handled = true; + } + } + + public async Task GetProfileItem() + { + var item = await ViewModel?.GetProfileItem(); + return item; + } + + public async Task?> GetProfileItems() + { + var item = await ViewModel?.GetProfileItems(); + return item; + } + + private void BtnSave_Click(object sender, RoutedEventArgs e) + { + // Trigger selection finalize when Confirm is clicked + ViewModel?.SelectFinish(); + } + #endregion Event +} diff --git a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs index 539886a8..c03fb0bd 100644 --- a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs +++ b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs @@ -29,7 +29,7 @@ public partial class ProfilesView btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; txtServerFilter.PreviewKeyDown += TxtServerFilter_PreviewKeyDown; lstProfiles.PreviewKeyDown += LstProfiles_PreviewKeyDown; - lstProfiles.SelectionChanged += lstProfiles_SelectionChanged; + lstProfiles.SelectionChanged += LstProfiles_SelectionChanged; lstProfiles.LoadingRow += LstProfiles_LoadingRow; menuSelectAll.Click += menuSelectAll_Click; @@ -191,7 +191,7 @@ public partial class ProfilesView } } - private void lstProfiles_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) + private void LstProfiles_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { if (ViewModel != null) { diff --git a/v2rayN/v2rayN/Views/RoutingRuleDetailsWindow.xaml b/v2rayN/v2rayN/Views/RoutingRuleDetailsWindow.xaml index baa4d543..f24a38d7 100644 --- a/v2rayN/v2rayN/Views/RoutingRuleDetailsWindow.xaml +++ b/v2rayN/v2rayN/Views/RoutingRuleDetailsWindow.xaml @@ -72,14 +72,24 @@ IsEditable="True" MaxDropDownHeight="1000" Style="{StaticResource DefComboBox}" /> - + Orientation="Horizontal"> +