Compare commits

..

8 commits

Author SHA1 Message Date
DHR60
519c48cab7
Merge 0085e0a507 into 6391667c15 2025-09-06 12:19:37 +00:00
DHR60
0085e0a507 Fix 2025-09-06 19:22:01 +08:00
DHR60
f9ea8d83d0 Allow single select 2025-09-06 19:04:37 +08:00
DHR60
cfcb7ae0e3 avalonia 2025-09-06 18:57:15 +08:00
DHR60
b7b5725dac wpf 2025-09-06 18:57:14 +08:00
DHR60
f2e27d2d6f Sort 2025-09-06 18:57:14 +08:00
DHR60
4d23911bfb Refactors ProfilesViewModel for code reuse 2025-09-06 18:56:52 +08:00
DHR60
34a93a307f Profiles Select Window 2025-09-06 17:02:09 +08:00
12 changed files with 442 additions and 623 deletions

View file

@ -28,7 +28,6 @@ Package: v2rayN
Version: $Version Version: $Version
Architecture: $Arch2 Architecture: $Arch2
Maintainer: https://github.com/2dust/v2rayN Maintainer: https://github.com/2dust/v2rayN
Depends: desktop-file-utils
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
EOF EOF

View file

@ -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<ProfileItem> _lstProfile = new();
protected string _serverFilter = string.Empty;
protected Dictionary<string, bool> _dicHeaderSort = new();
#endregion
#region Observable properties
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
[Reactive]
public ProfileItemModel SelectedProfile { get; set; }
public IList<ProfileItemModel> SelectedProfiles { get; set; }
[Reactive]
public SubItem SelectedSub { get; set; }
[Reactive]
public string ServerFilter { get; set; }
#endregion
protected ProfilesBaseViewModel(Func<EViewAction, object?, Task<bool>>? 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<ProfileItemModel>();
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(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<List<ProfileItemModel>?> 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
}

View file

@ -10,108 +10,23 @@ using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat; using Splat;
using ServiceLib.Base;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class ProfilesSelectViewModel : MyReactiveObject public class ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? updateView) : ProfilesBaseViewModel(updateView)
{ {
#region private prop #region private prop
private List<ProfileItem> _lstProfile;
private string _serverFilter = string.Empty;
private Dictionary<string, bool> _dicHeaderSort = new();
private string _subIndexId = string.Empty; private string _subIndexId = string.Empty;
// ConfigType filter state: default include-mode with all types selected
private List<EConfigType> _filterConfigTypes = new();
private bool _filterExclude = false;
#endregion private prop #endregion private prop
#region ObservableCollection
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
[Reactive]
public ProfileItemModel SelectedProfile { get; set; }
public IList<ProfileItemModel> SelectedProfiles { get; set; }
[Reactive]
public SubItem SelectedSub { get; set; }
[Reactive]
public string ServerFilter { get; set; }
// Include/Exclude filter for ConfigType
public List<EConfigType> 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 #region Init
protected override async Task Initialize()
public ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppManager.Instance.Config;
_updateView = updateView;
_subIndexId = _config.SubIndexId ?? string.Empty; _subIndexId = _config.SubIndexId ?? string.Empty;
#region WhenAnyValue && ReactiveCommand await base.Initialize();
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<EConfigType>().ToList();
}
catch
{
FilterConfigTypes = new();
}
await RefreshSubscriptions();
await RefreshServers();
}
#endregion Init #endregion Init
#region Actions #region Actions
@ -134,118 +49,6 @@ public class ProfilesSelectViewModel : MyReactiveObject
#region Servers && Groups #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<List<ProfileItem>>(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<List<ProfileItemModel>?> 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<ProfileItem?> GetProfileItem() public async Task<ProfileItem?> GetProfileItem()
{ {
if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
@ -262,44 +65,17 @@ public class ProfilesSelectViewModel : MyReactiveObject
return item; return item;
} }
public async Task<List<ProfileItem>?> GetProfileItems() public Task SortServer(string colName)
{
if (SelectedProfiles == null || SelectedProfiles.Count == 0)
{
return null;
}
var lst = new List<ProfileItem>();
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()) if (colName.IsNullOrEmpty())
{ {
return; return Task.CompletedTask;
} }
var prop = typeof(ProfileItemModel).GetProperty(colName); var prop = typeof(ProfileItemModel).GetProperty(colName);
if (prop == null) if (prop == null)
{ {
return; return Task.CompletedTask;
} }
_dicHeaderSort.TryAdd(colName, true); _dicHeaderSort.TryAdd(colName, true);
@ -341,19 +117,23 @@ public class ProfilesSelectViewModel : MyReactiveObject
_dicHeaderSort[colName] = !asc; _dicHeaderSort[colName] = !asc;
return; return Task.CompletedTask;
} }
#endregion Servers && Groups #endregion Servers && Groups
#region Public API #region overrides
// External setter for ConfigType filter protected override string GetCurrentSubIndexId()
public void SetConfigTypeFilter(IEnumerable<EConfigType> types, bool exclude = false)
{ {
FilterConfigTypes = types?.Distinct().ToList() ?? new List<EConfigType>(); return _subIndexId;
FilterExclude = exclude; }
protected override void SetCurrentSubIndexId(string? id)
{
_subIndexId = id ?? string.Empty;
} }
#endregion Public API protected override bool ShouldSetDefaultServer => false;
#endregion overrides
} }

View file

@ -7,40 +7,23 @@ using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat; using Splat;
using ServiceLib.Base;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class ProfilesViewModel : MyReactiveObject public class ProfilesViewModel : ProfilesBaseViewModel
{ {
#region private prop #region private prop
private List<ProfileItem> _lstProfile;
private string _serverFilter = string.Empty;
private Dictionary<string, bool> _dicHeaderSort = new();
private SpeedtestService? _speedtestService; private SpeedtestService? _speedtestService;
#endregion private prop #endregion private prop
#region ObservableCollection #region ObservableCollection
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
[Reactive]
public ProfileItemModel SelectedProfile { get; set; }
public IList<ProfileItemModel> SelectedProfiles { get; set; }
[Reactive]
public SubItem SelectedSub { get; set; }
[Reactive] [Reactive]
public SubItem SelectedMoveToGroup { get; set; } public SubItem SelectedMoveToGroup { get; set; }
[Reactive]
public string ServerFilter { get; set; }
#endregion ObservableCollection #endregion ObservableCollection
#region Menu #region Menu
@ -89,175 +72,61 @@ public class ProfilesViewModel : MyReactiveObject
#region Init #region Init
public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? updateView) : base(updateView)
{ {
_config = AppManager.Instance.Config;
_updateView = updateView;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedProfile, x => x.SelectedProfile,
selectedSource => selectedSource != null && !selectedSource.IndexId.IsNullOrEmpty()); 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( this.WhenAnyValue(
x => x.SelectedMoveToGroup, x => x.SelectedMoveToGroup,
y => y != null && !y.Remarks.IsNullOrEmpty()) y => y != null && !y.Remarks.IsNullOrEmpty())
.Subscribe(async c => await MoveToGroup(c)); .Subscribe(async c => await MoveToGroup(c));
this.WhenAnyValue(
x => x.ServerFilter,
y => y != null && _serverFilter != y)
.Subscribe(async c => await ServerFilterChanged(c));
//servers delete //servers delete
EditServerCmd = ReactiveCommand.CreateFromTask(async () => EditServerCmd = ReactiveCommand.CreateFromTask(EditServerAsync, canEditRemove);
{ RemoveServerCmd = ReactiveCommand.CreateFromTask(RemoveServerAsync, canEditRemove);
await EditServerAsync(EConfigType.Custom); RemoveDuplicateServerCmd = ReactiveCommand.CreateFromTask(RemoveDuplicateServer);
}, canEditRemove); CopyServerCmd = ReactiveCommand.CreateFromTask(CopyServer, canEditRemove);
RemoveServerCmd = ReactiveCommand.CreateFromTask(async () => SetDefaultServerCmd = ReactiveCommand.CreateFromTask(SetDefaultServer, canEditRemove);
{ ShareServerCmd = ReactiveCommand.CreateFromTask(ShareServerAsync, canEditRemove);
await RemoveServerAsync(); SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(() =>
}, canEditRemove); SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random), canEditRemove);
RemoveDuplicateServerCmd = ReactiveCommand.CreateFromTask(async () => SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(() =>
{ SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin), canEditRemove);
await RemoveDuplicateServer(); SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(() =>
}); SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing), canEditRemove);
CopyServerCmd = ReactiveCommand.CreateFromTask(async () => SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(() =>
{ SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad), canEditRemove);
await CopyServer(); SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(() =>
}, canEditRemove); SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing), 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);
//servers move //servers move
MoveTopCmd = ReactiveCommand.CreateFromTask(async () => MoveTopCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Top), canEditRemove);
{ MoveUpCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Up), canEditRemove);
await MoveServer(EMove.Top); MoveDownCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Down), canEditRemove);
}, canEditRemove); MoveBottomCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Bottom), 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);
//servers ping //servers ping
MixedTestServerCmd = ReactiveCommand.CreateFromTask(async () => MixedTestServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Mixedtest));
{ TcpingServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Tcping), canEditRemove);
await ServerSpeedtest(ESpeedActionType.Mixedtest); RealPingServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Realping), canEditRemove);
}); SpeedServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Speedtest), canEditRemove);
TcpingServerCmd = ReactiveCommand.CreateFromTask(async () => SortServerResultCmd = ReactiveCommand.CreateFromTask(() => SortServer(EServerColName.DelayVal.ToString()));
{ RemoveInvalidServerResultCmd = ReactiveCommand.CreateFromTask(RemoveInvalidServerResult);
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();
});
//servers export //servers export
Export2ClientConfigCmd = ReactiveCommand.CreateFromTask(async () => Export2ClientConfigCmd = ReactiveCommand.CreateFromTask(() => Export2ClientConfigAsync(false), canEditRemove);
{ Export2ClientConfigClipboardCmd = ReactiveCommand.CreateFromTask(() => Export2ClientConfigAsync(true), canEditRemove);
await Export2ClientConfigAsync(false); Export2ShareUrlCmd = ReactiveCommand.CreateFromTask(() => Export2ShareUrlAsync(false), canEditRemove);
}, canEditRemove); Export2ShareUrlBase64Cmd = ReactiveCommand.CreateFromTask(() => Export2ShareUrlAsync(true), 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);
//Subscription //Subscription
AddSubCmd = ReactiveCommand.CreateFromTask(async () => AddSubCmd = ReactiveCommand.CreateFromTask(() => EditSubAsync(true));
{ EditSubCmd = ReactiveCommand.CreateFromTask(() => EditSubAsync(false));
await EditSubAsync(true);
});
EditSubCmd = ReactiveCommand.CreateFromTask(async () =>
{
await EditSubAsync(false);
});
#endregion WhenAnyValue && ReactiveCommand #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();
await RefreshSubscriptions();
await RefreshServers();
} }
#endregion Init #endregion Init
@ -269,18 +138,18 @@ public class ProfilesViewModel : MyReactiveObject
Locator.Current.GetService<MainWindowViewModel>()?.Reload(); Locator.Current.GetService<MainWindowViewModel>()?.Reload();
} }
public async Task SetSpeedTestResult(SpeedTestResult result) public Task SetSpeedTestResult(SpeedTestResult result)
{ {
if (result.IndexId.IsNullOrEmpty()) if (result.IndexId.IsNullOrEmpty())
{ {
NoticeManager.Instance.SendMessageEx(result.Delay); NoticeManager.Instance.SendMessageEx(result.Delay);
NoticeManager.Instance.Enqueue(result.Delay); NoticeManager.Instance.Enqueue(result.Delay);
return; return Task.CompletedTask;
} }
var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId); var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId);
if (item == null) if (item == null)
{ {
return; return Task.CompletedTask;
} }
if (result.Delay.IsNotEmpty()) if (result.Delay.IsNotEmpty())
@ -294,165 +163,16 @@ public class ProfilesViewModel : MyReactiveObject
item.SpeedVal = result.Speed ?? string.Empty; item.SpeedVal = result.Speed ?? string.Empty;
} }
//_profileItems.Replace(item, JsonUtils.DeepCopy(item)); //_profileItems.Replace(item, JsonUtils.DeepCopy(item));
} return Task.CompletedTask;
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
{
}
} }
#endregion Actions #endregion Actions
#region Servers && Groups protected override async Task Initialize()
private async Task SubSelectedChangedAsync(bool c)
{ {
if (!c) await base.Initialize();
{ SelectedMoveToGroup = new();
return;
} }
_config.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()
{
AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default);
await Task.Delay(200);
}
private async Task RefreshServersBiz()
{
var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter);
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(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<List<ProfileItemModel>?> 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 #region Add Servers
@ -484,7 +204,7 @@ public class ProfilesViewModel : MyReactiveObject
return lstSelected; return lstSelected;
} }
public async Task EditServerAsync(EConfigType eConfigType) public async Task EditServerAsync()
{ {
if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
{ {
@ -496,7 +216,7 @@ public class ProfilesViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return; return;
} }
eConfigType = item.ConfigType; var eConfigType = item.ConfigType;
bool? ret = false; bool? ret = false;
if (eConfigType == EConfigType.Custom) if (eConfigType == EConfigType.Custom)
@ -517,6 +237,11 @@ public class ProfilesViewModel : MyReactiveObject
} }
} }
public Task EditServerAsync(EConfigType _)
{
return EditServerAsync();
}
public async Task RemoveServerAsync() public async Task RemoveServerAsync()
{ {
var lstSelected = await GetProfileItems(true); var lstSelected = await GetProfileItems(true);
@ -651,7 +376,7 @@ public class ProfilesViewModel : MyReactiveObject
} }
_dicHeaderSort.TryAdd(colName, true); _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) if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0)
{ {
return; return;
@ -733,13 +458,14 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
_speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) => _speedtestService ??= new SpeedtestService(_config, (SpeedTestResult result) =>
{ {
RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) => RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) =>
{ {
_ = SetSpeedTestResult(result); _ = SetSpeedTestResult(result);
return Disposable.Empty; return Disposable.Empty;
}); });
return Task.CompletedTask;
}); });
_speedtestService?.RunLoop(actionType, lstSelected); _speedtestService?.RunLoop(actionType, lstSelected);
} }
@ -853,4 +579,27 @@ public class ProfilesViewModel : MyReactiveObject
} }
#endregion Subscription #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
} }

View file

@ -2,6 +2,7 @@
x:Class="v2rayN.Desktop.Views.ProfilesSelectWindow" x:Class="v2rayN.Desktop.Views.ProfilesSelectWindow"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="using:v2rayN.Desktop.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@ -10,9 +11,12 @@
Width="800" Width="800"
Height="450" Height="450"
x:DataType="vms:ProfilesSelectViewModel" x:DataType="vms:ProfilesSelectViewModel"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.Resources>
<conv:DelayColorConverter x:Key="DelayColorConverter" />
</Window.Resources>
<DockPanel Margin="8"> <DockPanel Margin="8">
<!-- Bottom buttons --> <!-- Bottom buttons -->
<StackPanel <StackPanel
@ -90,7 +94,7 @@
Header="{x:Static resx:ResUI.LvServiceType}" Header="{x:Static resx:ResUI.LvServiceType}"
Tag="ConfigType" /> Tag="ConfigType" />
<DataGridTemplateColumn SortMemberPath="Remarks" Tag="Remarks"> <DataGridTemplateColumn Tag="Remarks">
<DataGridTemplateColumn.Header> <DataGridTemplateColumn.Header>
<TextBlock Text="{x:Static resx:ResUI.LvRemarks}" /> <TextBlock Text="{x:Static resx:ResUI.LvRemarks}" />
</DataGridTemplateColumn.Header> </DataGridTemplateColumn.Header>
@ -128,6 +132,49 @@
Binding="{Binding SubRemarks}" Binding="{Binding SubRemarks}"
Header="{x:Static resx:ResUI.LvSubscription}" Header="{x:Static resx:ResUI.LvSubscription}"
Tag="SubRemarks" /> Tag="SubRemarks" />
<DataGridTemplateColumn SortMemberPath="Delay" Tag="DelayVal">
<DataGridTemplateColumn.Header>
<TextBlock Text="{x:Static resx:ResUI.LvTestDelay}" />
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock
Margin="8,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Foreground="{Binding Delay, Converter={StaticResource DelayColorConverter}}"
Text="{Binding DelayVal}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="100"
Binding="{Binding SpeedVal}"
Header="{x:Static resx:ResUI.LvTestSpeed}"
Tag="SpeedVal" />
<DataGridTextColumn
Width="100"
Binding="{Binding TodayUp}"
Header="{x:Static resx:ResUI.LvTodayUploadDataAmount}"
Tag="TodayUp" />
<DataGridTextColumn
Width="100"
Binding="{Binding TodayDown}"
Header="{x:Static resx:ResUI.LvTodayDownloadDataAmount}"
Tag="TodayDown" />
<DataGridTextColumn
Width="100"
Binding="{Binding TotalUp}"
Header="{x:Static resx:ResUI.LvTotalUploadDataAmount}"
Tag="TotalUp" />
<DataGridTextColumn
Width="100"
Binding="{Binding TotalDown}"
Header="{x:Static resx:ResUI.LvTotalDownloadDataAmount}"
Tag="TotalDown" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</DockPanel> </DockPanel>

View file

@ -3,11 +3,9 @@ using System.Reactive.Disposables;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager; using ServiceLib.Manager;
@ -19,8 +17,7 @@ public partial class ProfilesSelectWindow : ReactiveWindow<ProfilesSelectViewMod
{ {
private static Config _config; private static Config _config;
public Task<ProfileItem?> ProfileItem => GetProfileItem(); public Task<ProfileItem?> ProfileItem => GetFirstProfileItemAsync();
public Task<List<ProfileItem>?> ProfileItems => GetProfileItems();
private bool _allowMultiSelect = false; private bool _allowMultiSelect = false;
public ProfilesSelectWindow() public ProfilesSelectWindow()
@ -49,6 +46,7 @@ public partial class ProfilesSelectWindow : ReactiveWindow<ProfilesSelectViewMod
this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).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); btnCancel.Click += (s, e) => Close(false);
} }
@ -72,10 +70,6 @@ public partial class ProfilesSelectWindow : ReactiveWindow<ProfilesSelectViewMod
} }
} }
// Expose ConfigType filter controls to callers
public void SetConfigTypeFilter(IEnumerable<EConfigType> types, bool exclude = false)
=> ViewModel?.SetConfigTypeFilter(types, exclude);
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{ {
switch (action) switch (action)
@ -101,33 +95,18 @@ public partial class ProfilesSelectWindow : ReactiveWindow<ProfilesSelectViewMod
} }
private void LstProfiles_DoubleTapped(object? sender, TappedEventArgs e) private void LstProfiles_DoubleTapped(object? sender, TappedEventArgs e)
{
// 忽略表头区域的双击
if (e.Source is Control src)
{
if (src.FindAncestorOfType<DataGridColumnHeader>() != null)
{
e.Handled = true;
return;
}
// 仅当在数据行或其子元素上双击时才触发选择
if (src.FindAncestorOfType<DataGridRow>() != null)
{ {
ViewModel?.SelectFinish(); ViewModel?.SelectFinish();
e.Handled = true;
}
}
} }
private void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e) private async void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e)
{ {
// 自定义排序,防止默认行为导致误触发
e.Handled = true; e.Handled = true;
if (ViewModel != null && e.Column?.Tag?.ToString() != null) if (ViewModel != null && e.Column?.Tag?.ToString() != null)
{ {
ViewModel.SortServer(e.Column.Tag.ToString()); await ViewModel.SortServer(e.Column.Tag.ToString());
} }
e.Handled = false;
} }
private void LstProfiles_KeyDown(object? sender, KeyEventArgs e) private void LstProfiles_KeyDown(object? sender, KeyEventArgs e)
@ -180,18 +159,12 @@ public partial class ProfilesSelectWindow : ReactiveWindow<ProfilesSelectViewMod
} }
} }
public async Task<ProfileItem?> GetProfileItem() public async Task<ProfileItem?> GetFirstProfileItemAsync()
{ {
var item = await ViewModel?.GetProfileItem(); var item = await ViewModel?.GetProfileItem();
return item; return item;
} }
public async Task<List<ProfileItem>?> GetProfileItems()
{
var item = await ViewModel?.GetProfileItems();
return item;
}
private void BtnSave_Click(object sender, RoutedEventArgs e) private void BtnSave_Click(object sender, RoutedEventArgs e)
{ {
// Trigger selection finalize when Confirm is clicked // Trigger selection finalize when Confirm is clicked

View file

@ -98,7 +98,6 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
private async void BtnSelectProfile_Click(object? sender, RoutedEventArgs e) private async void BtnSelectProfile_Click(object? sender, RoutedEventArgs e)
{ {
var selectWindow = new ProfilesSelectWindow(); var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
var result = await selectWindow.ShowDialog<bool?>(this); var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true) if (result == true)
{ {

View file

@ -64,7 +64,6 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e) private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e)
{ {
var selectWindow = new ProfilesSelectWindow(); var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
var result = await selectWindow.ShowDialog<bool?>(this); var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true) if (result == true)
{ {
@ -79,7 +78,6 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
private async void BtnSelectNextProfile_Click(object? sender, RoutedEventArgs e) private async void BtnSelectNextProfile_Click(object? sender, RoutedEventArgs e)
{ {
var selectWindow = new ProfilesSelectWindow(); var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
var result = await selectWindow.ShowDialog<bool?>(this); var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true) if (result == true)
{ {

View file

@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:v2rayN.Base" xmlns:base="clr-namespace:v2rayN.Base"
xmlns:conv="clr-namespace:v2rayN.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -14,9 +15,15 @@
Height="450" Height="450"
x:TypeArguments="vms:ProfilesSelectViewModel" x:TypeArguments="vms:ProfilesSelectViewModel"
Style="{StaticResource WindowGlobal}" Style="{StaticResource WindowGlobal}"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.Resources>
<ResourceDictionary>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" />
<conv:DelayColorConverter x:Key="DelayColorConverter" />
</ResourceDictionary>
</Window.Resources>
<DockPanel Margin="{StaticResource Margin8}"> <DockPanel Margin="{StaticResource Margin8}">
<StackPanel <StackPanel
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@ -26,9 +33,9 @@
<Button <Button
x:Name="btnSave" x:Name="btnSave"
Width="100" Width="100"
Click="BtnSave_Click"
Content="{x:Static resx:ResUI.TbConfirm}" Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" IsDefault="True"
Click="BtnSave_Click"
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
@ -148,6 +155,55 @@
Binding="{Binding SubRemarks}" Binding="{Binding SubRemarks}"
ExName="SubRemarks" ExName="SubRemarks"
Header="{x:Static resx:ResUI.LvSubscription}" /> Header="{x:Static resx:ResUI.LvSubscription}" />
<base:MyDGTextColumn
Width="100"
Binding="{Binding DelayVal}"
ExName="DelayVal"
Header="{x:Static resx:ResUI.LvTestDelay}"
SortMemberPath="Delay">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="Foreground" Value="{Binding Delay, Converter={StaticResource DelayColorConverter}}" />
</Style>
</DataGridTextColumn.ElementStyle>
</base:MyDGTextColumn>
<base:MyDGTextColumn
Width="100"
Binding="{Binding SpeedVal}"
ExName="SpeedVal"
Header="{x:Static resx:ResUI.LvTestSpeed}">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
</DataGridTextColumn.ElementStyle>
</base:MyDGTextColumn>
<base:MyDGTextColumn
x:Name="colTodayUp"
Width="100"
Binding="{Binding TodayUp}"
ExName="TodayUp"
Header="{x:Static resx:ResUI.LvTodayUploadDataAmount}" />
<base:MyDGTextColumn
x:Name="colTodayDown"
Width="100"
Binding="{Binding TodayDown}"
ExName="TodayDown"
Header="{x:Static resx:ResUI.LvTodayDownloadDataAmount}" />
<base:MyDGTextColumn
x:Name="colTotalUp"
Width="100"
Binding="{Binding TotalUp}"
ExName="TotalUp"
Header="{x:Static resx:ResUI.LvTotalUploadDataAmount}" />
<base:MyDGTextColumn
x:Name="colTotalDown"
Width="100"
Binding="{Binding TotalDown}"
ExName="TotalDown"
Header="{x:Static resx:ResUI.LvTotalDownloadDataAmount}" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</DockPanel> </DockPanel>

View file

@ -15,8 +15,7 @@ public partial class ProfilesSelectWindow
{ {
private static Config _config; private static Config _config;
public Task<ProfileItem?> ProfileItem => GetProfileItem(); public Task<ProfileItem?> ProfileItem => GetFirstProfileItemAsync();
public Task<List<ProfileItem>?> ProfileItems => GetProfileItems();
private bool _allowMultiSelect = false; private bool _allowMultiSelect = false;
public ProfilesSelectWindow() public ProfilesSelectWindow()
@ -66,10 +65,6 @@ public partial class ProfilesSelectWindow
} }
} }
// Expose ConfigType filter controls to callers
public void SetConfigTypeFilter(IEnumerable<EConfigType> types, bool exclude = false)
=> ViewModel?.SetConfigTypeFilter(types, exclude);
#region Event #region Event
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@ -173,18 +168,12 @@ public partial class ProfilesSelectWindow
} }
} }
public async Task<ProfileItem?> GetProfileItem() public async Task<ProfileItem?> GetFirstProfileItemAsync()
{ {
var item = await ViewModel?.GetProfileItem(); var item = await ViewModel?.GetProfileItem();
return item; return item;
} }
public async Task<List<ProfileItem>?> GetProfileItems()
{
var item = await ViewModel?.GetProfileItems();
return item;
}
private void BtnSave_Click(object sender, RoutedEventArgs e) private void BtnSave_Click(object sender, RoutedEventArgs e)
{ {
// Trigger selection finalize when Confirm is clicked // Trigger selection finalize when Confirm is clicked

View file

@ -93,7 +93,6 @@ public partial class RoutingRuleDetailsWindow
private async void BtnSelectProfile_Click(object sender, RoutedEventArgs e) private async void BtnSelectProfile_Click(object sender, RoutedEventArgs e)
{ {
var selectWindow = new ProfilesSelectWindow(); var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
if (selectWindow.ShowDialog() == true) if (selectWindow.ShowDialog() == true)
{ {
var profile = await selectWindow.ProfileItem; var profile = await selectWindow.ProfileItem;

View file

@ -58,7 +58,6 @@ public partial class SubEditWindow
private async void BtnSelectPrevProfile_Click(object sender, RoutedEventArgs e) private async void BtnSelectPrevProfile_Click(object sender, RoutedEventArgs e)
{ {
var selectWindow = new ProfilesSelectWindow(); var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
if (selectWindow.ShowDialog() == true) if (selectWindow.ShowDialog() == true)
{ {
var profile = await selectWindow.ProfileItem; var profile = await selectWindow.ProfileItem;
@ -72,7 +71,6 @@ public partial class SubEditWindow
private async void BtnSelectNextProfile_Click(object sender, RoutedEventArgs e) private async void BtnSelectNextProfile_Click(object sender, RoutedEventArgs e)
{ {
var selectWindow = new ProfilesSelectWindow(); var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
if (selectWindow.ShowDialog() == true) if (selectWindow.ShowDialog() == true)
{ {
var profile = await selectWindow.ProfileItem; var profile = await selectWindow.ProfileItem;