mirror of
https://github.com/2dust/v2rayN.git
synced 2025-10-27 18:42:52 +00:00
Merge 0085e0a507 into 6391667c15
This commit is contained in:
commit
519c48cab7
23 changed files with 1363 additions and 359 deletions
9
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
9
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
|
|
@ -3030,6 +3030,15 @@ namespace ServiceLib.Resx {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Select Profile 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSelectProfile {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSelectProfile", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Set system proxy 的本地化字符串。
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1512,4 +1512,7 @@
|
|||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>Start parsing and processing subscription content</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1512,4 +1512,7 @@
|
|||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>Start parsing and processing subscription content</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1512,4 +1512,7 @@
|
|||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>Start parsing and processing subscription content</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1512,4 +1512,7 @@
|
|||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>Start parsing and processing subscription content</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1509,4 +1509,7 @@
|
|||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>开始解析和处理订阅内容</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>选择配置文件</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1509,4 +1509,7 @@
|
|||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>開始解析和處理訂閱內容</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
</root>
|
||||
232
v2rayN/ServiceLib/ViewModels/ProfilesBaseViewModel.cs
Normal file
232
v2rayN/ServiceLib/ViewModels/ProfilesBaseViewModel.cs
Normal 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
|
||||
}
|
||||
139
v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs
Normal file
139
v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs
Normal file
|
|
@ -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<EViewAction, object?, Task<bool>>? 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<ProfileItem?> 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<object?>.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<ProfileItemModel> 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
|
||||
}
|
||||
|
|
@ -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<ProfileItem> _lstProfile;
|
||||
private string _serverFilter = string.Empty;
|
||||
private Dictionary<string, bool> _dicHeaderSort = new();
|
||||
private SpeedtestService? _speedtestService;
|
||||
|
||||
#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 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<EViewAction, object?, Task<bool>>? updateView)
|
||||
public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? 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<MainWindowViewModel>()?.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<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
|
||||
|
||||
private async Task<List<ProfileItem>?> 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
|
||||
}
|
||||
|
|
|
|||
183
v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml
Normal file
183
v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<Window
|
||||
x:Class="v2rayN.Desktop.Views.ProfilesSelectWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
|
||||
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
|
||||
Title="{x:Static resx:ResUI.TbSelectProfile}"
|
||||
Width="800"
|
||||
Height="450"
|
||||
x:DataType="vms:ProfilesSelectViewModel"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Window.Resources>
|
||||
<conv:DelayColorConverter x:Key="DelayColorConverter" />
|
||||
</Window.Resources>
|
||||
|
||||
<DockPanel Margin="8">
|
||||
<!-- Bottom buttons -->
|
||||
<StackPanel
|
||||
Margin="4"
|
||||
HorizontalAlignment="Center"
|
||||
DockPanel.Dock="Bottom"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="btnSave"
|
||||
Width="100"
|
||||
Click="BtnSave_Click"
|
||||
Content="{x:Static resx:ResUI.TbConfirm}" />
|
||||
<Button
|
||||
x:Name="btnCancel"
|
||||
Width="100"
|
||||
Margin="8,0"
|
||||
Content="{x:Static resx:ResUI.TbCancel}" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid>
|
||||
<DockPanel>
|
||||
<!-- Top tools -->
|
||||
<WrapPanel Margin="4" DockPanel.Dock="Top">
|
||||
<ListBox
|
||||
x:Name="lstGroup"
|
||||
Margin="4,0"
|
||||
DisplayMemberBinding="{Binding Remarks}"
|
||||
ItemsSource="{Binding SubItems}">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
</ListBox>
|
||||
|
||||
<Button
|
||||
x:Name="btnAutofitColumnWidth"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="8,0"
|
||||
ToolTip.Tip="{x:Static resx:ResUI.menuProfileAutofitColumnWidth}">
|
||||
<Button.Content>
|
||||
<PathIcon Data="{StaticResource building_fit}" />
|
||||
</Button.Content>
|
||||
</Button>
|
||||
|
||||
<TextBox
|
||||
x:Name="txtServerFilter"
|
||||
Width="200"
|
||||
Margin="8,0"
|
||||
VerticalContentAlignment="Center"
|
||||
Text="{Binding ServerFilter, Mode=TwoWay}"
|
||||
Watermark="{x:Static resx:ResUI.MsgServerTitle}" />
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Profiles grid -->
|
||||
<DataGrid
|
||||
x:Name="lstProfiles"
|
||||
AutoGenerateColumns="False"
|
||||
BorderThickness="1"
|
||||
CanUserReorderColumns="True"
|
||||
CanUserResizeColumns="True"
|
||||
GridLinesVisibility="All"
|
||||
HeadersVisibility="All"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding ProfileItems}"
|
||||
SelectionMode="Single">
|
||||
<DataGrid.KeyBindings>
|
||||
<KeyBinding Command="{Binding SelectFinish}" Gesture="Enter" />
|
||||
</DataGrid.KeyBindings>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn
|
||||
Width="80"
|
||||
Binding="{Binding ConfigType}"
|
||||
Header="{x:Static resx:ResUI.LvServiceType}"
|
||||
Tag="ConfigType" />
|
||||
|
||||
<DataGridTemplateColumn Tag="Remarks">
|
||||
<DataGridTemplateColumn.Header>
|
||||
<TextBlock Text="{x:Static resx:ResUI.LvRemarks}" />
|
||||
</DataGridTemplateColumn.Header>
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="8,0" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Remarks}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
Binding="{Binding Address}"
|
||||
Header="{x:Static resx:ResUI.LvAddress}"
|
||||
Tag="Address" />
|
||||
<DataGridTextColumn
|
||||
Width="60"
|
||||
Binding="{Binding Port}"
|
||||
Header="{x:Static resx:ResUI.LvPort}"
|
||||
Tag="Port" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
Binding="{Binding Network}"
|
||||
Header="{x:Static resx:ResUI.LvTransportProtocol}"
|
||||
Tag="Network" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
Binding="{Binding StreamSecurity}"
|
||||
Header="{x:Static resx:ResUI.LvTLS}"
|
||||
Tag="StreamSecurity" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
Binding="{Binding SubRemarks}"
|
||||
Header="{x:Static resx:ResUI.LvSubscription}"
|
||||
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>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
173
v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs
Normal file
173
v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs
Normal file
|
|
@ -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<ProfilesSelectViewModel>
|
||||
{
|
||||
private static Config _config;
|
||||
|
||||
public Task<ProfileItem?> 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<bool> 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<ProfileItemModel>().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<ProfileItem?> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -54,13 +54,22 @@
|
|||
Width="300"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Text="{Binding SelectedSource.OutboundTag, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
|
||||
VerticalAlignment="Center">
|
||||
<Button
|
||||
x:Name="btnSelectProfile"
|
||||
Margin="0,0,8,0"
|
||||
Content="{x:Static resx:ResUI.TbSelectProfile}"
|
||||
Click="BtnSelectProfile_Click" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Avalonia.Controls;
|
|||
using Avalonia.Interactivity;
|
||||
using ReactiveUI;
|
||||
using v2rayN.Desktop.Base;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace v2rayN.Desktop.Views;
|
||||
|
||||
|
|
@ -93,4 +94,18 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
|
|||
{
|
||||
ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject");
|
||||
}
|
||||
|
||||
private async void BtnSelectProfile_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectWindow = new ProfilesSelectWindow();
|
||||
var result = await selectWindow.ShowDialog<bool?>(this);
|
||||
if (result == true)
|
||||
{
|
||||
var profile = await selectWindow.ProfileItem;
|
||||
if (profile != null)
|
||||
{
|
||||
cmbOutboundTag.Text = profile.Remarks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,6 +204,12 @@
|
|||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Watermark="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||
<Button
|
||||
Grid.Row="9"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Content="{x:Static resx:ResUI.TbSelectProfile}"
|
||||
Click="BtnSelectPrevProfile_Click" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="10"
|
||||
|
|
@ -218,6 +224,12 @@
|
|||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Watermark="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||
<Button
|
||||
Grid.Row="10"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Content="{x:Static resx:ResUI.TbSelectProfile}"
|
||||
Click="BtnSelectNextProfile_Click" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="11"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Avalonia;
|
|||
using Avalonia.Interactivity;
|
||||
using ReactiveUI;
|
||||
using v2rayN.Desktop.Base;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace v2rayN.Desktop.Views;
|
||||
|
||||
|
|
@ -59,4 +60,32 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
|||
{
|
||||
txtRemarks.Focus();
|
||||
}
|
||||
|
||||
private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectWindow = new ProfilesSelectWindow();
|
||||
var result = await selectWindow.ShowDialog<bool?>(this);
|
||||
if (result == true)
|
||||
{
|
||||
var profile = await selectWindow.ProfileItem;
|
||||
if (profile != null)
|
||||
{
|
||||
txtPrevProfile.Text = profile.Remarks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void BtnSelectNextProfile_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectWindow = new ProfilesSelectWindow();
|
||||
var result = await selectWindow.ShowDialog<bool?>(this);
|
||||
if (result == true)
|
||||
{
|
||||
var profile = await selectWindow.ProfileItem;
|
||||
if (profile != null)
|
||||
{
|
||||
txtNextProfile.Text = profile.Remarks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
212
v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml
Normal file
212
v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
<base:WindowBase
|
||||
x:Class="v2rayN.Views.ProfilesSelectWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:base="clr-namespace:v2rayN.Base"
|
||||
xmlns:conv="clr-namespace:v2rayN.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:reactiveui="http://reactiveui.net"
|
||||
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
|
||||
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
|
||||
Title="{x:Static resx:ResUI.TbSelectProfile}"
|
||||
Width="800"
|
||||
Height="450"
|
||||
x:TypeArguments="vms:ProfilesSelectViewModel"
|
||||
Style="{StaticResource WindowGlobal}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" />
|
||||
<conv:DelayColorConverter x:Key="DelayColorConverter" />
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
|
||||
<DockPanel Margin="{StaticResource Margin8}">
|
||||
<StackPanel
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Center"
|
||||
DockPanel.Dock="Bottom"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="btnSave"
|
||||
Width="100"
|
||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||
IsDefault="True"
|
||||
Click="BtnSave_Click"
|
||||
Style="{StaticResource DefButton}" />
|
||||
<Button
|
||||
x:Name="btnCancel"
|
||||
Width="100"
|
||||
Margin="{StaticResource MarginLeftRight8}"
|
||||
Content="{x:Static resx:ResUI.TbCancel}"
|
||||
IsCancel="true"
|
||||
Style="{StaticResource DefButton}" />
|
||||
</StackPanel>
|
||||
<Grid>
|
||||
<DockPanel>
|
||||
<WrapPanel Margin="{StaticResource Margin4}" DockPanel.Dock="Top">
|
||||
<ListBox
|
||||
x:Name="lstGroup"
|
||||
MaxHeight="200"
|
||||
AutomationProperties.Name="{x:Static resx:ResUI.menuSubscription}"
|
||||
FontSize="{DynamicResource StdFontSize}"
|
||||
ItemContainerStyle="{StaticResource MyChipListBoxItem}"
|
||||
Style="{StaticResource MaterialDesignChoiceChipPrimaryOutlineListBox}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Remarks}" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<Button
|
||||
x:Name="btnAutofitColumnWidth"
|
||||
Width="30"
|
||||
Height="30"
|
||||
Margin="{StaticResource MarginLeftRight8}"
|
||||
AutomationProperties.Name="{x:Static resx:ResUI.menuProfileAutofitColumnWidth}"
|
||||
Style="{StaticResource MaterialDesignFloatingActionMiniLightButton}"
|
||||
ToolTip="{x:Static resx:ResUI.menuProfileAutofitColumnWidth}">
|
||||
<materialDesign:PackIcon VerticalAlignment="Center" Kind="ArrowSplitVertical" />
|
||||
</Button>
|
||||
<TextBox
|
||||
x:Name="txtServerFilter"
|
||||
Width="200"
|
||||
Margin="{StaticResource MarginLeftRight4}"
|
||||
VerticalContentAlignment="Center"
|
||||
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.MsgServerTitle}"
|
||||
materialDesign:TextFieldAssist.HasClearButton="True"
|
||||
AutomationProperties.Name="{x:Static resx:ResUI.MsgServerTitle}"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
</WrapPanel>
|
||||
<DataGrid
|
||||
x:Name="lstProfiles"
|
||||
materialDesign:DataGridAssist.CellPadding="2,2"
|
||||
AutoGenerateColumns="False"
|
||||
BorderThickness="1"
|
||||
CanUserAddRows="False"
|
||||
CanUserResizeRows="False"
|
||||
CanUserSortColumns="False"
|
||||
EnableRowVirtualization="True"
|
||||
Focusable="True"
|
||||
GridLinesVisibility="All"
|
||||
HeadersVisibility="All"
|
||||
IsReadOnly="True"
|
||||
RowHeaderWidth="40"
|
||||
SelectionMode="Single"
|
||||
Style="{StaticResource DefDataGrid}">
|
||||
<DataGrid.InputBindings>
|
||||
<KeyBinding Command="ApplicationCommands.NotACommand" Gesture="Enter" />
|
||||
</DataGrid.InputBindings>
|
||||
<DataGrid.Resources>
|
||||
<Style BasedOn="{StaticResource MaterialDesignDataGridRow}" TargetType="DataGridRow">
|
||||
<EventSetter Event="MouseDoubleClick" Handler="LstProfiles_MouseDoubleClick" />
|
||||
</Style>
|
||||
<Style BasedOn="{StaticResource MaterialDesignDataGridColumnHeader}" TargetType="DataGridColumnHeader">
|
||||
<EventSetter Event="Click" Handler="LstProfiles_ColumnHeader_Click" />
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource MaterialDesignDataGridCell}" TargetType="DataGridCell">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsActive}" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource MaterialDesign.Brush.Primary.Light}" />
|
||||
<Setter Property="Foreground" Value="Black" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource MaterialDesign.Brush.Primary.Light}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.Columns>
|
||||
<base:MyDGTextColumn
|
||||
Width="80"
|
||||
Binding="{Binding ConfigType}"
|
||||
ExName="ConfigType"
|
||||
Header="{x:Static resx:ResUI.LvServiceType}" />
|
||||
<base:MyDGTextColumn
|
||||
Width="150"
|
||||
Binding="{Binding Remarks}"
|
||||
ExName="Remarks"
|
||||
Header="{x:Static resx:ResUI.LvRemarks}" />
|
||||
<base:MyDGTextColumn
|
||||
Width="120"
|
||||
Binding="{Binding Address}"
|
||||
ExName="Address"
|
||||
Header="{x:Static resx:ResUI.LvAddress}" />
|
||||
<base:MyDGTextColumn
|
||||
Width="60"
|
||||
Binding="{Binding Port}"
|
||||
ExName="Port"
|
||||
Header="{x:Static resx:ResUI.LvPort}" />
|
||||
<base:MyDGTextColumn
|
||||
Width="100"
|
||||
Binding="{Binding Network}"
|
||||
ExName="Network"
|
||||
Header="{x:Static resx:ResUI.LvTransportProtocol}" />
|
||||
<base:MyDGTextColumn
|
||||
Width="100"
|
||||
Binding="{Binding StreamSecurity}"
|
||||
ExName="StreamSecurity"
|
||||
Header="{x:Static resx:ResUI.LvTLS}" />
|
||||
<base:MyDGTextColumn
|
||||
Width="100"
|
||||
Binding="{Binding SubRemarks}"
|
||||
ExName="SubRemarks"
|
||||
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>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</base:WindowBase>
|
||||
183
v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs
Normal file
183
v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs
Normal file
|
|
@ -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?> 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<bool> 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<ProfileItemModel>().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<ProfileItem?> 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -72,14 +72,24 @@
|
|||
IsEditable="True"
|
||||
MaxDropDownHeight="1000"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
<TextBlock
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="{StaticResource Margin4}"
|
||||
Click="BtnSelectProfile_Click"
|
||||
Content="{x:Static resx:ResUI.TbSelectProfile}"
|
||||
Style="{StaticResource DefButton}" />
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Reactive.Disposables;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using ReactiveUI;
|
||||
using ServiceLib.Manager;
|
||||
|
||||
|
|
@ -88,4 +89,17 @@ public partial class RoutingRuleDetailsWindow
|
|||
{
|
||||
ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject");
|
||||
}
|
||||
|
||||
private async void BtnSelectProfile_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectWindow = new ProfilesSelectWindow();
|
||||
if (selectWindow.ShowDialog() == true)
|
||||
{
|
||||
var profile = await selectWindow.ProfileItem;
|
||||
if (profile != null)
|
||||
{
|
||||
cmbOutboundTag.Text = profile.Remarks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,6 +259,14 @@
|
|||
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.LvPrevProfileTip}"
|
||||
AcceptsReturn="True"
|
||||
Style="{StaticResource MyOutlinedTextBox}" />
|
||||
<Button
|
||||
Grid.Row="9"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Click="BtnSelectPrevProfile_Click"
|
||||
Content="{x:Static resx:ResUI.TbSelectProfile}"
|
||||
Style="{StaticResource DefButton}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="10"
|
||||
|
|
@ -276,6 +284,14 @@
|
|||
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.LvPrevProfileTip}"
|
||||
AcceptsReturn="True"
|
||||
Style="{StaticResource MyOutlinedTextBox}" />
|
||||
<Button
|
||||
Grid.Row="10"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Click="BtnSelectNextProfile_Click"
|
||||
Content="{x:Static resx:ResUI.TbSelectProfile}"
|
||||
Style="{StaticResource DefButton}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="11"
|
||||
|
|
|
|||
|
|
@ -54,4 +54,30 @@ public partial class SubEditWindow
|
|||
{
|
||||
txtRemarks.Focus();
|
||||
}
|
||||
|
||||
private async void BtnSelectPrevProfile_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectWindow = new ProfilesSelectWindow();
|
||||
if (selectWindow.ShowDialog() == true)
|
||||
{
|
||||
var profile = await selectWindow.ProfileItem;
|
||||
if (profile != null)
|
||||
{
|
||||
txtPrevProfile.Text = profile.Remarks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void BtnSelectNextProfile_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectWindow = new ProfilesSelectWindow();
|
||||
if (selectWindow.ShowDialog() == true)
|
||||
{
|
||||
var profile = await selectWindow.ProfileItem;
|
||||
if (profile != null)
|
||||
{
|
||||
txtNextProfile.Text = profile.Remarks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue