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