From 5db990a2f8836ed818932e6acfb2472b0a584656 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Sat, 6 Sep 2025 17:02:09 +0800 Subject: [PATCH] Profiles Select Window --- .../ViewModels/ProfilesSelectViewModel.cs | 298 ++++++++++++++++++ .../ViewModels/ProfilesViewModel.cs | 25 -- v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml | 210 ++++++++++++ .../v2rayN/Views/ProfilesSelectWindow.xaml.cs | 150 +++++++++ v2rayN/v2rayN/Views/ProfilesView.xaml.cs | 4 +- v2rayN/v2rayN/Views/SubEditWindow.xaml | 16 + v2rayN/v2rayN/Views/SubEditWindow.xaml.cs | 26 ++ 7 files changed, 702 insertions(+), 27 deletions(-) create mode 100644 v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs create mode 100644 v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml create mode 100644 v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs new file mode 100644 index 00000000..a9ab2dd4 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -0,0 +1,298 @@ +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; + + #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; } + + #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)); + + #endregion WhenAnyValue && ReactiveCommand + + #region AppEvents + + AppEvents.ProfilesRefreshRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await RefreshServersBiz()); + + AppEvents.DispatcherStatisticsRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(UpdateStatistics); + + #endregion AppEvents + + _ = Init(); + } + + private async Task Init() + { + SelectedProfile = new(); + SelectedSub = new(); + + await RefreshSubscriptions(); + await RefreshServers(); + } + + #endregion Init + + #region Actions + + public void UpdateStatistics(ServerSpeedItem update) + { + if (!_config.GuiItem.EnableStatistics + || (update.ProxyUp + update.ProxyDown) <= 0 + || DateTime.Now.Second % 3 != 0) + { + return; + } + + try + { + var item = ProfileItems.FirstOrDefault(it => it.IndexId == update.IndexId); + if (item != null) + { + item.TodayDown = Utils.HumanFy(update.TodayDown); + item.TodayUp = Utils.HumanFy(update.TodayUp); + item.TotalDown = Utils.HumanFy(update.TotalDown); + item.TotalUp = Utils.HumanFy(update.TotalUp); + + //if (SelectedProfile?.IndexId == item.IndexId) + //{ + // var temp = JsonUtils.DeepCopy(item); + // _profileItems.Replace(item, temp); + // SelectedProfile = temp; + //} + //else + //{ + // _profileItems.Replace(item, JsonUtils.DeepCopy(item)); + //} + } + } + catch + { + } + } + + 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); + + var lstServerStat = (_config.GuiItem.EnableStatistics ? StatisticsManager.Instance.ServerStat : null) ?? []; + var lstProfileExs = await ProfileExManager.Instance.GetProfileExs(); + lstModel = (from t in lstModel + join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b + from t22 in t2b.DefaultIfEmpty() + join t3 in lstProfileExs on t.IndexId equals t3.IndexId into t3b + from t33 in t3b.DefaultIfEmpty() + 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, + Sort = t33?.Sort ?? 0, + Delay = t33?.Delay ?? 0, + Speed = t33?.Speed ?? 0, + DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty, + SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty, + TodayDown = t22 == null ? "" : Utils.HumanFy(t22.TodayDown), + TodayUp = t22 == null ? "" : Utils.HumanFy(t22.TodayUp), + TotalDown = t22 == null ? "" : Utils.HumanFy(t22.TotalDown), + TotalUp = t22 == null ? "" : Utils.HumanFy(t22.TotalUp) + }).OrderBy(t => t.Sort).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 SortServer(string colName) + { + //if (colName.IsNullOrEmpty()) + //{ + // return; + //} + + //_dicHeaderSort.TryAdd(colName, true); + //_dicHeaderSort.TryGetValue(colName, out bool asc); + //if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0) + //{ + // return; + //} + //_dicHeaderSort[colName] = !asc; + //await RefreshServers(); + } + + #endregion Servers && Groups +} 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/Views/ProfilesSelectWindow.xaml b/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml new file mode 100644 index 00000000..fe639716 --- /dev/null +++ b/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs b/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs new file mode 100644 index 00000000..c315ad2d --- /dev/null +++ b/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs @@ -0,0 +1,150 @@ +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 => GetFirstProfileItemAsync(); + + 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); + }); + + } + + #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) + { + 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); + break; + } + } + else + { + if (e.Key is Key.Enter or Key.Return) + { + ViewModel?.SelectFinish(); + } + } + } + + 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(); + } + } + + public async Task GetFirstProfileItemAsync() + { + var item = await ViewModel?.GetProfileItem(); + return item; + } + #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/SubEditWindow.xaml b/v2rayN/v2rayN/Views/SubEditWindow.xaml index 2d863a96..01db868f 100644 --- a/v2rayN/v2rayN/Views/SubEditWindow.xaml +++ b/v2rayN/v2rayN/Views/SubEditWindow.xaml @@ -259,6 +259,14 @@ materialDesign:HintAssist.Hint="{x:Static resx:ResUI.LvPrevProfileTip}" AcceptsReturn="True" Style="{StaticResource MyOutlinedTextBox}" /> +