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