Compare commits

..

13 commits

Author SHA1 Message Date
DHR60
3a06ff8113 Remove unnecessary 2025-09-07 18:02:00 +08:00
DHR60
c9ec34b836 Add Config Type Filter 2025-09-07 15:51:10 +08:00
DHR60
f11b46ed37 Fix 2025-09-07 12:03:15 +08:00
DHR60
e6c57fcac0 Allow single select 2025-09-07 12:03:15 +08:00
DHR60
1242011513 avalonia 2025-09-07 12:03:15 +08:00
DHR60
9a4d8faaa2 wpf 2025-09-07 12:03:14 +08:00
DHR60
0302b9d1d0 Sort 2025-09-07 12:03:14 +08:00
DHR60
5db990a2f8 Profiles Select Window 2025-09-07 12:03:14 +08:00
2dust
6391667c15 up 7.14.7
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-06 16:59:54 +08:00
2dust
7f26445327 Update Directory.Packages.props 2025-09-06 16:59:38 +08:00
2dust
291d4bd8e5 Update Directory.Packages.props 2025-09-06 16:52:57 +08:00
dependabot[bot]
f2f3a7eb5f
Bump actions/setup-dotnet from 4.3.1 to 5.0.0 (#7883)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4.3.1 to 5.0.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4.3.1...v5.0.0)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-06 15:46:34 +08:00
JieXu
e7609619d4
Update package-debian.sh (#7888) 2025-09-06 15:46:20 +08:00
18 changed files with 643 additions and 453 deletions

View file

@ -32,7 +32,7 @@ jobs:
fetch-depth: '0'
- name: Setup
uses: actions/setup-dotnet@v4.3.1
uses: actions/setup-dotnet@v5.0.0
with:
dotnet-version: '8.0.x'

View file

@ -32,7 +32,7 @@ jobs:
fetch-depth: '0'
- name: Setup
uses: actions/setup-dotnet@v4.3.1
uses: actions/setup-dotnet@v5.0.0
with:
dotnet-version: '8.0.x'

View file

@ -32,7 +32,7 @@ jobs:
fetch-depth: '0'
- name: Setup
uses: actions/setup-dotnet@v4.3.1
uses: actions/setup-dotnet@v5.0.0
with:
dotnet-version: '8.0.x'

View file

@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v5.0.0
- name: Setup
uses: actions/setup-dotnet@v4.3.1
uses: actions/setup-dotnet@v5.0.0
with:
dotnet-version: '8.0.x'

View file

@ -52,7 +52,17 @@ sudo chmod 0755 "${PackagePath}/DEBIAN/postinst"
sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN"
sudo chmod 0755 "${PackagePath}/opt/v2rayN/AmazTool"
# desktop && PATH
# Patch
# set owner to root:root
sudo chown -R root:root "${PackagePath}"
# set all directories to 755 (readable & traversable by all users)
sudo find "${PackagePath}/opt/v2rayN" -type d -exec chmod 755 {} +
# set all regular files to 644 (readable by all users)
sudo find "${PackagePath}/opt/v2rayN" -type f -exec chmod 644 {} +
# ensure main binaries are 755 (executable by all users)
sudo chmod 755 "${PackagePath}/opt/v2rayN/v2rayN" 2>/dev/null || true
sudo chmod 755 "${PackagePath}/opt/v2rayN/AmazTool" 2>/dev/null || true
# build deb package
sudo dpkg-deb -Zxz --build $PackagePath
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb"

View file

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.14.6</Version>
<Version>7.14.7</Version>
</PropertyGroup>
<PropertyGroup>

View file

@ -20,7 +20,7 @@
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.9" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.9" />
<PackageVersion Include="Splat.NLog" Version="15.5.3" />
<PackageVersion Include="Splat.NLog" Version="16.2.1" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />

View file

@ -1,232 +0,0 @@
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,23 +10,108 @@ 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)
public class ProfilesSelectViewModel : MyReactiveObject
{
#region private prop
private List<ProfileItem> _lstProfile;
private string _serverFilter = string.Empty;
private Dictionary<string, bool> _dicHeaderSort = new();
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
#region Init
protected override async Task Initialize()
#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
{
_subIndexId = _config.SubIndexId ?? string.Empty;
await base.Initialize();
get => _filterConfigTypes;
set => this.RaiseAndSetIfChanged(ref _filterConfigTypes, value);
}
[Reactive]
public bool FilterExclude
{
get => _filterExclude;
set => this.RaiseAndSetIfChanged(ref _filterExclude, value);
}
#endregion ObservableCollection
#region Init
public ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{
_config = AppManager.Instance.Config;
_updateView = updateView;
_subIndexId = _config.SubIndexId ?? string.Empty;
#region WhenAnyValue && ReactiveCommand
this.WhenAnyValue(
x => x.SelectedSub,
y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id)
.Subscribe(async c => await SubSelectedChangedAsync(c));
this.WhenAnyValue(
x => x.ServerFilter,
y => y != null && _serverFilter != y)
.Subscribe(async c => await ServerFilterChanged(c));
// React to ConfigType filter changes
this.WhenAnyValue(x => x.FilterExclude)
.Skip(1)
.Subscribe(async _ => await RefreshServersBiz());
this.WhenAnyValue(x => x.FilterConfigTypes)
.Skip(1)
.Subscribe(async _ => await RefreshServersBiz());
#endregion WhenAnyValue && ReactiveCommand
_ = Init();
}
private async Task Init()
{
SelectedProfile = new();
SelectedSub = new();
// Default: include mode with all ConfigTypes selected
try
{
FilterExclude = false;
FilterConfigTypes = Enum.GetValues(typeof(EConfigType)).Cast<EConfigType>().ToList();
}
catch
{
FilterConfigTypes = new();
}
await RefreshSubscriptions();
await RefreshServers();
}
#endregion Init
#region Actions
@ -49,6 +134,118 @@ public class ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? upd
#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()
{
if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
@ -65,17 +262,44 @@ public class ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? upd
return item;
}
public Task SortServer(string colName)
public async Task<List<ProfileItem>?> GetProfileItems()
{
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())
{
return Task.CompletedTask;
return;
}
var prop = typeof(ProfileItemModel).GetProperty(colName);
if (prop == null)
{
return Task.CompletedTask;
return;
}
_dicHeaderSort.TryAdd(colName, true);
@ -117,23 +341,19 @@ public class ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? upd
_dicHeaderSort[colName] = !asc;
return Task.CompletedTask;
return;
}
#endregion Servers && Groups
#region overrides
#region Public API
protected override string GetCurrentSubIndexId()
// External setter for ConfigType filter
public void SetConfigTypeFilter(IEnumerable<EConfigType> types, bool exclude = false)
{
return _subIndexId;
}
protected override void SetCurrentSubIndexId(string? id)
{
_subIndexId = id ?? string.Empty;
FilterConfigTypes = types?.Distinct().ToList() ?? new List<EConfigType>();
FilterExclude = exclude;
}
protected override bool ShouldSetDefaultServer => false;
#endregion overrides
#endregion Public API
}

View file

@ -7,23 +7,40 @@ using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;
using ServiceLib.Base;
namespace ServiceLib.ViewModels;
public class ProfilesViewModel : ProfilesBaseViewModel
public class ProfilesViewModel : MyReactiveObject
{
#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 string ServerFilter { get; set; }
#endregion ObservableCollection
#region Menu
@ -72,61 +89,175 @@ public class ProfilesViewModel : ProfilesBaseViewModel
#region Init
public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? updateView) : base(updateView)
public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? 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.ServerFilter,
y => y != null && _serverFilter != y)
.Subscribe(async c => await ServerFilterChanged(c));
//servers delete
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);
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);
//servers move
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);
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);
//servers ping
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);
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();
});
//servers export
Export2ClientConfigCmd = ReactiveCommand.CreateFromTask(() => Export2ClientConfigAsync(false), canEditRemove);
Export2ClientConfigClipboardCmd = ReactiveCommand.CreateFromTask(() => Export2ClientConfigAsync(true), canEditRemove);
Export2ShareUrlCmd = ReactiveCommand.CreateFromTask(() => Export2ShareUrlAsync(false), canEditRemove);
Export2ShareUrlBase64Cmd = ReactiveCommand.CreateFromTask(() => Export2ShareUrlAsync(true), canEditRemove);
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);
//Subscription
AddSubCmd = ReactiveCommand.CreateFromTask(() => EditSubAsync(true));
EditSubCmd = ReactiveCommand.CreateFromTask(() => EditSubAsync(false));
AddSubCmd = ReactiveCommand.CreateFromTask(async () =>
{
await EditSubAsync(true);
});
EditSubCmd = ReactiveCommand.CreateFromTask(async () =>
{
await 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();
await RefreshSubscriptions();
await RefreshServers();
}
#endregion Init
@ -138,18 +269,18 @@ public class ProfilesViewModel : ProfilesBaseViewModel
Locator.Current.GetService<MainWindowViewModel>()?.Reload();
}
public Task SetSpeedTestResult(SpeedTestResult result)
public async Task SetSpeedTestResult(SpeedTestResult result)
{
if (result.IndexId.IsNullOrEmpty())
{
NoticeManager.Instance.SendMessageEx(result.Delay);
NoticeManager.Instance.Enqueue(result.Delay);
return Task.CompletedTask;
return;
}
var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId);
if (item == null)
{
return Task.CompletedTask;
return;
}
if (result.Delay.IsNotEmpty())
@ -163,17 +294,166 @@ public class ProfilesViewModel : ProfilesBaseViewModel
item.SpeedVal = result.Speed ?? string.Empty;
}
//_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
protected override async Task Initialize()
#region Servers && Groups
private async Task SubSelectedChangedAsync(bool c)
{
await base.Initialize();
SelectedMoveToGroup = new();
if (!c)
{
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
private async Task<List<ProfileItem>?> GetProfileItems(bool latest)
@ -204,7 +484,7 @@ public class ProfilesViewModel : ProfilesBaseViewModel
return lstSelected;
}
public async Task EditServerAsync()
public async Task EditServerAsync(EConfigType eConfigType)
{
if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
{
@ -216,7 +496,7 @@ public class ProfilesViewModel : ProfilesBaseViewModel
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return;
}
var eConfigType = item.ConfigType;
eConfigType = item.ConfigType;
bool? ret = false;
if (eConfigType == EConfigType.Custom)
@ -237,11 +517,6 @@ public class ProfilesViewModel : ProfilesBaseViewModel
}
}
public Task EditServerAsync(EConfigType _)
{
return EditServerAsync();
}
public async Task RemoveServerAsync()
{
var lstSelected = await GetProfileItems(true);
@ -376,7 +651,7 @@ public class ProfilesViewModel : ProfilesBaseViewModel
}
_dicHeaderSort.TryAdd(colName, true);
_dicHeaderSort.TryGetValue(colName, out var asc);
_dicHeaderSort.TryGetValue(colName, out bool asc);
if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0)
{
return;
@ -458,14 +733,13 @@ public class ProfilesViewModel : ProfilesBaseViewModel
return;
}
_speedtestService ??= new SpeedtestService(_config, (SpeedTestResult result) =>
_speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) =>
{
RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) =>
{
_ = SetSpeedTestResult(result);
return Disposable.Empty;
});
return Task.CompletedTask;
});
_speedtestService?.RunLoop(actionType, lstSelected);
}
@ -579,27 +853,4 @@ public class ProfilesViewModel : ProfilesBaseViewModel
}
#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,7 +2,6 @@
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"
@ -11,12 +10,9 @@
Width="800"
Height="450"
x:DataType="vms:ProfilesSelectViewModel"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.Resources>
<conv:DelayColorConverter x:Key="DelayColorConverter" />
</Window.Resources>
<DockPanel Margin="8">
<!-- Bottom buttons -->
<StackPanel
@ -94,7 +90,7 @@
Header="{x:Static resx:ResUI.LvServiceType}"
Tag="ConfigType" />
<DataGridTemplateColumn Tag="Remarks">
<DataGridTemplateColumn SortMemberPath="Remarks" Tag="Remarks">
<DataGridTemplateColumn.Header>
<TextBlock Text="{x:Static resx:ResUI.LvRemarks}" />
</DataGridTemplateColumn.Header>
@ -132,49 +128,6 @@
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>

View file

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

View file

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

View file

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

View file

@ -3,7 +3,6 @@
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"
@ -15,15 +14,9 @@
Height="450"
x:TypeArguments="vms:ProfilesSelectViewModel"
Style="{StaticResource WindowGlobal}"
WindowStartupLocation="CenterScreen"
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}"
@ -33,9 +26,9 @@
<Button
x:Name="btnSave"
Width="100"
Click="BtnSave_Click"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Click="BtnSave_Click"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
@ -155,55 +148,6 @@
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>

View file

@ -15,7 +15,8 @@ public partial class ProfilesSelectWindow
{
private static Config _config;
public Task<ProfileItem?> ProfileItem => GetFirstProfileItemAsync();
public Task<ProfileItem?> ProfileItem => GetProfileItem();
public Task<List<ProfileItem>?> ProfileItems => GetProfileItems();
private bool _allowMultiSelect = false;
public ProfilesSelectWindow()
@ -65,6 +66,10 @@ 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
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@ -168,12 +173,18 @@ public partial class ProfilesSelectWindow
}
}
public async Task<ProfileItem?> GetFirstProfileItemAsync()
public async Task<ProfileItem?> GetProfileItem()
{
var item = await ViewModel?.GetProfileItem();
return item;
}
public async Task<List<ProfileItem>?> GetProfileItems()
{
var item = await ViewModel?.GetProfileItems();
return item;
}
private void BtnSave_Click(object sender, RoutedEventArgs e)
{
// Trigger selection finalize when Confirm is clicked

View file

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

View file

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