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/ProfilesBaseViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesBaseViewModel.cs new file mode 100644 index 00000000..778ea4b2 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/ProfilesBaseViewModel.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using ServiceLib.Base; + +namespace ServiceLib.ViewModels; + +public abstract class ProfilesBaseViewModel : MyReactiveObject +{ + #region protected fields + + protected List _lstProfile = new(); + protected string _serverFilter = string.Empty; + protected Dictionary _dicHeaderSort = new(); + + #endregion + + #region Observable properties + + 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 + + protected ProfilesBaseViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + this.WhenAnyValue( + x => x.SelectedSub, + y => y != null && !y.Remarks.IsNullOrEmpty() && GetCurrentSubIndexId() != y.Id) + .Subscribe(async c => await SubSelectedChangedAsync(c)); + + this.WhenAnyValue( + x => x.ServerFilter, + y => y != null && _serverFilter != y) + .Subscribe(async c => await ServerFilterChanged(c)); + + AppEvents.ProfilesRefreshRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await RefreshServersBiz()); + + AppEvents.DispatcherStatisticsRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(UpdateStatistics); + + _ = Initialize(); + } + + protected virtual async Task Initialize() + { + SelectedProfile = new(); + SelectedSub = new(); + + await RefreshSubscriptions(); + await RefreshServers(); + } + + #region Hook points + + protected abstract string GetCurrentSubIndexId(); + protected abstract void SetCurrentSubIndexId(string? id); + + protected virtual bool ShouldSetDefaultServer => false; + + public virtual async Task RefreshServers() + { + await RefreshServersBiz(); + } + + #endregion + + #region Shared actions + + protected virtual async Task SubSelectedChangedAsync(bool changed) + { + if (!changed) + { + return; + } + + SetCurrentSubIndexId(SelectedSub?.Id); + + await RefreshServers(); + + await _updateView?.Invoke(EViewAction.ProfilesFocus, null); + } + + protected virtual async Task ServerFilterChanged(bool changed) + { + if (!changed) + { + return; + } + _serverFilter = ServerFilter; + if (_serverFilter.IsNullOrEmpty()) + { + await RefreshServers(); + } + } + + protected async Task RefreshServersBiz() + { + var lstModel = await GetProfileItemsEx(GetCurrentSubIndexId(), _serverFilter) ?? new List(); + + _lstProfile = JsonUtils.Deserialize>(JsonUtils.Serialize(lstModel)) ?? []; + + ProfileItems.Clear(); + ProfileItems.AddRange(lstModel); + if (lstModel.Count > 0) + { + var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); + SelectedProfile = selected ?? lstModel.First(); + } + + await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); + } + + public virtual async Task RefreshSubscriptions() + { + SubItems.Clear(); + + SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers }); + + foreach (var item in await AppManager.Instance.SubItems()) + { + SubItems.Add(item); + } + + var curId = GetCurrentSubIndexId(); + if (!curId.IsNullOrEmpty() && SubItems.FirstOrDefault(t => t.Id == curId) != null) + { + SelectedSub = SubItems.FirstOrDefault(t => t.Id == curId); + } + else + { + SelectedSub = SubItems.First(); + } + } + + protected virtual async Task?> GetProfileItemsEx(string subid, string filter) + { + var lstModel = await AppManager.Instance.ProfileItems(subid, filter); + + if (ShouldSetDefaultServer) + { + 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 virtual 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); + } + } + catch + { + } + } + + #endregion +} diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs new file mode 100644 index 00000000..0cf7b994 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -0,0 +1,139 @@ +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; +using ServiceLib.Base; + +namespace ServiceLib.ViewModels; +public class ProfilesSelectViewModel(Func>? updateView) : ProfilesBaseViewModel(updateView) +{ + #region private prop + + private string _subIndexId = string.Empty; + + #endregion private prop + + #region Init + protected override async Task Initialize() + { + _subIndexId = _config.SubIndexId ?? string.Empty; + await base.Initialize(); + } + #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 + + 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 Task SortServer(string colName) + { + if (colName.IsNullOrEmpty()) + { + return Task.CompletedTask; + } + + var prop = typeof(ProfileItemModel).GetProperty(colName); + if (prop == null) + { + return Task.CompletedTask; + } + + _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 Task.CompletedTask; + } + + #endregion Servers && Groups + + #region overrides + + protected override string GetCurrentSubIndexId() + { + return _subIndexId; + } + protected override void SetCurrentSubIndexId(string? id) + { + _subIndexId = id ?? string.Empty; + } + + protected override bool ShouldSetDefaultServer => false; + + #endregion overrides +} diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 5209f4dd..dce27dd1 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -7,46 +7,23 @@ using DynamicData.Binding; using ReactiveUI; using ReactiveUI.Fody.Helpers; using Splat; +using ServiceLib.Base; namespace ServiceLib.ViewModels; -public class ProfilesViewModel : MyReactiveObject +public class ProfilesViewModel : ProfilesBaseViewModel { #region private prop - private List _lstProfile; - private string _serverFilter = string.Empty; - private Dictionary _dicHeaderSort = new(); private SpeedtestService? _speedtestService; #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 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 @@ -95,181 +72,61 @@ public class ProfilesViewModel : MyReactiveObject #region Init - public ProfilesViewModel(Func>? updateView) + public ProfilesViewModel(Func>? updateView) : base(updateView) { - _config = AppManager.Instance.Config; - _updateView = updateView; - #region WhenAnyValue && ReactiveCommand var canEditRemove = this.WhenAnyValue( x => x.SelectedProfile, selectedSource => selectedSource != null && !selectedSource.IndexId.IsNullOrEmpty()); - this.WhenAnyValue( - x => x.SelectedSub, - y => y != null && !y.Remarks.IsNullOrEmpty() && _config.SubIndexId != y.Id) - .Subscribe(async c => await SubSelectedChangedAsync(c)); this.WhenAnyValue( x => x.SelectedMoveToGroup, 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) - .Subscribe(async c => await ServerFilterChanged(c)); - //servers delete - EditServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await EditServerAsync(EConfigType.Custom); - }, canEditRemove); - RemoveServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await RemoveServerAsync(); - }, canEditRemove); - RemoveDuplicateServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await RemoveDuplicateServer(); - }); - CopyServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await CopyServer(); - }, canEditRemove); - SetDefaultServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await SetDefaultServer(); - }, canEditRemove); - ShareServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await ShareServerAsync(); - }, canEditRemove); - SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => - { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random); - }, canEditRemove); - SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => - { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); - }, canEditRemove); - SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => - { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); - }, canEditRemove); - SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => - { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); - }, canEditRemove); - SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => - { - await SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); - }, canEditRemove); + EditServerCmd = ReactiveCommand.CreateFromTask(EditServerAsync, canEditRemove); + RemoveServerCmd = ReactiveCommand.CreateFromTask(RemoveServerAsync, canEditRemove); + RemoveDuplicateServerCmd = ReactiveCommand.CreateFromTask(RemoveDuplicateServer); + CopyServerCmd = ReactiveCommand.CreateFromTask(CopyServer, canEditRemove); + SetDefaultServerCmd = ReactiveCommand.CreateFromTask(SetDefaultServer, canEditRemove); + ShareServerCmd = ReactiveCommand.CreateFromTask(ShareServerAsync, canEditRemove); + SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(() => + SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random), canEditRemove); + SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(() => + SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin), canEditRemove); + SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(() => + SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing), canEditRemove); + SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(() => + SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad), canEditRemove); + SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(() => + SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing), canEditRemove); //servers move - MoveTopCmd = ReactiveCommand.CreateFromTask(async () => - { - await MoveServer(EMove.Top); - }, canEditRemove); - MoveUpCmd = ReactiveCommand.CreateFromTask(async () => - { - await MoveServer(EMove.Up); - }, canEditRemove); - MoveDownCmd = ReactiveCommand.CreateFromTask(async () => - { - await MoveServer(EMove.Down); - }, canEditRemove); - MoveBottomCmd = ReactiveCommand.CreateFromTask(async () => - { - await MoveServer(EMove.Bottom); - }, canEditRemove); + MoveTopCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Top), canEditRemove); + MoveUpCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Up), canEditRemove); + MoveDownCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Down), canEditRemove); + MoveBottomCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Bottom), canEditRemove); //servers ping - MixedTestServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await ServerSpeedtest(ESpeedActionType.Mixedtest); - }); - TcpingServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await ServerSpeedtest(ESpeedActionType.Tcping); - }, canEditRemove); - RealPingServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await ServerSpeedtest(ESpeedActionType.Realping); - }, canEditRemove); - SpeedServerCmd = ReactiveCommand.CreateFromTask(async () => - { - await ServerSpeedtest(ESpeedActionType.Speedtest); - }, canEditRemove); - SortServerResultCmd = ReactiveCommand.CreateFromTask(async () => - { - await SortServer(EServerColName.DelayVal.ToString()); - }); - RemoveInvalidServerResultCmd = ReactiveCommand.CreateFromTask(async () => - { - await RemoveInvalidServerResult(); - }); + MixedTestServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Mixedtest)); + TcpingServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Tcping), canEditRemove); + RealPingServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Realping), canEditRemove); + SpeedServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Speedtest), canEditRemove); + SortServerResultCmd = ReactiveCommand.CreateFromTask(() => SortServer(EServerColName.DelayVal.ToString())); + RemoveInvalidServerResultCmd = ReactiveCommand.CreateFromTask(RemoveInvalidServerResult); //servers export - Export2ClientConfigCmd = ReactiveCommand.CreateFromTask(async () => - { - await Export2ClientConfigAsync(false); - }, canEditRemove); - Export2ClientConfigClipboardCmd = ReactiveCommand.CreateFromTask(async () => - { - await Export2ClientConfigAsync(true); - }, canEditRemove); - Export2ShareUrlCmd = ReactiveCommand.CreateFromTask(async () => - { - await Export2ShareUrlAsync(false); - }, canEditRemove); - Export2ShareUrlBase64Cmd = ReactiveCommand.CreateFromTask(async () => - { - await Export2ShareUrlAsync(true); - }, canEditRemove); + Export2ClientConfigCmd = ReactiveCommand.CreateFromTask(() => Export2ClientConfigAsync(false), canEditRemove); + Export2ClientConfigClipboardCmd = ReactiveCommand.CreateFromTask(() => Export2ClientConfigAsync(true), canEditRemove); + Export2ShareUrlCmd = ReactiveCommand.CreateFromTask(() => Export2ShareUrlAsync(false), canEditRemove); + Export2ShareUrlBase64Cmd = ReactiveCommand.CreateFromTask(() => Export2ShareUrlAsync(true), canEditRemove); //Subscription - AddSubCmd = ReactiveCommand.CreateFromTask(async () => - { - await EditSubAsync(true); - }); - EditSubCmd = ReactiveCommand.CreateFromTask(async () => - { - await EditSubAsync(false); - }); + AddSubCmd = ReactiveCommand.CreateFromTask(() => EditSubAsync(true)); + EditSubCmd = ReactiveCommand.CreateFromTask(() => EditSubAsync(false)); #endregion WhenAnyValue && ReactiveCommand - - #region AppEvents - - AppEvents.ProfilesRefreshRequested - .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(async _ => await RefreshServersBiz()); - - AppEvents.DispatcherStatisticsRequested - .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(async result => await UpdateStatistics(result)); - - #endregion AppEvents - - _ = Init(); - } - - private async Task Init() - { - SelectedProfile = new(); - SelectedSub = new(); - SelectedMoveToGroup = new(); - SelectedServer = new(); - - await RefreshSubscriptions(); - await RefreshServers(); } #endregion Init @@ -281,18 +138,18 @@ public class ProfilesViewModel : MyReactiveObject Locator.Current.GetService()?.Reload(); } - public async Task SetSpeedTestResult(SpeedTestResult result) + public Task SetSpeedTestResult(SpeedTestResult result) { if (result.IndexId.IsNullOrEmpty()) { NoticeManager.Instance.SendMessageEx(result.Delay); NoticeManager.Instance.Enqueue(result.Delay); - return; + return Task.CompletedTask; } var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId); if (item == null) { - return; + return Task.CompletedTask; } if (result.Delay.IsNotEmpty()) @@ -306,166 +163,17 @@ public class ProfilesViewModel : MyReactiveObject item.SpeedVal = result.Speed ?? string.Empty; } //_profileItems.Replace(item, JsonUtils.DeepCopy(item)); - } - - public async Task 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 - { - } + return Task.CompletedTask; } #endregion Actions - #region Servers && Groups - - private async Task SubSelectedChangedAsync(bool c) + protected override async Task Initialize() { - if (!c) - { - return; - } - _config.SubIndexId = SelectedSub?.Id; - - await RefreshServers(); - - await _updateView?.Invoke(EViewAction.ProfilesFocus, null); + await base.Initialize(); + SelectedMoveToGroup = new(); } - private async Task ServerFilterChanged(bool c) - { - if (!c) - { - return; - } - _serverFilter = ServerFilter; - if (_serverFilter.IsNullOrEmpty()) - { - await RefreshServers(); - } - } - - public async Task RefreshServers() - { - AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default); - - await Task.Delay(200); - } - - private async Task RefreshServersBiz() - { - var lstModel = await GetProfileItemsEx(_config.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 (_config.SubIndexId != null && SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null) - { - SelectedSub = SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId); - } - else - { - SelectedSub = SubItems.First(); - } - } - - private async Task?> GetProfileItemsEx(string subid, string filter) - { - var lstModel = await AppManager.Instance.ProfileItems(_config.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; - } - - #endregion Servers && Groups - #region Add Servers private async Task?> GetProfileItems(bool latest) @@ -496,7 +204,7 @@ public class ProfilesViewModel : MyReactiveObject return lstSelected; } - public async Task EditServerAsync(EConfigType eConfigType) + public async Task EditServerAsync() { if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) { @@ -508,7 +216,7 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } - eConfigType = item.ConfigType; + var eConfigType = item.ConfigType; bool? ret = false; if (eConfigType == EConfigType.Custom) @@ -529,6 +237,11 @@ public class ProfilesViewModel : MyReactiveObject } } + public Task EditServerAsync(EConfigType _) + { + return EditServerAsync(); + } + public async Task RemoveServerAsync() { var lstSelected = await GetProfileItems(true); @@ -613,19 +326,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); @@ -676,7 +376,7 @@ public class ProfilesViewModel : MyReactiveObject } _dicHeaderSort.TryAdd(colName, true); - _dicHeaderSort.TryGetValue(colName, out bool asc); + _dicHeaderSort.TryGetValue(colName, out var asc); if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0) { return; @@ -758,13 +458,14 @@ public class ProfilesViewModel : MyReactiveObject return; } - _speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) => + _speedtestService ??= new SpeedtestService(_config, (SpeedTestResult result) => { RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) => { _ = SetSpeedTestResult(result); return Disposable.Empty; }); + return Task.CompletedTask; }); _speedtestService?.RunLoop(actionType, lstSelected); } @@ -878,4 +579,27 @@ public class ProfilesViewModel : MyReactiveObject } #endregion Subscription + + #region overrides + + protected override string GetCurrentSubIndexId() + { + return _config.SubIndexId; + } + + protected override void SetCurrentSubIndexId(string? id) + { + _config.SubIndexId = id; + } + + public override async Task RefreshServers() + { + AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default); + await Task.Delay(200); + await base.RefreshServers(); + } + + protected override bool ShouldSetDefaultServer => true; + + #endregion overrides } diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml new file mode 100644 index 00000000..a50f0d7f --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs new file mode 100644 index 00000000..8aafbd83 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs @@ -0,0 +1,173 @@ +using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +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 => GetFirstProfileItemAsync(); + 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); + }); + + btnSave.Click += (s, e) => ViewModel?.SelectFinish(); + 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; + } + } + } + + 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) + { + ViewModel?.SelectFinish(); + } + + 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; + } + + 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 GetFirstProfileItemAsync() + { + var item = await ViewModel?.GetProfileItem(); + 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..58c903bb --- /dev/null +++ b/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs @@ -0,0 +1,183 @@ +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(); + 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; + } + } + } + + #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 GetFirstProfileItemAsync() + { + var item = await ViewModel?.GetProfileItem(); + 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"> +