mirror of
https://github.com/2dust/v2rayN.git
synced 2025-10-27 18:42:52 +00:00
Compare commits
13 commits
0085e0a507
...
3a06ff8113
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a06ff8113 | ||
|
|
c9ec34b836 | ||
|
|
f11b46ed37 | ||
|
|
e6c57fcac0 | ||
|
|
1242011513 | ||
|
|
9a4d8faaa2 | ||
|
|
0302b9d1d0 | ||
|
|
5db990a2f8 | ||
|
|
6391667c15 | ||
|
|
7f26445327 | ||
|
|
291d4bd8e5 | ||
|
|
f2f3a7eb5f | ||
|
|
e7609619d4 |
18 changed files with 643 additions and 453 deletions
2
.github/workflows/build-linux.yml
vendored
2
.github/workflows/build-linux.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: actions/setup-dotnet@v4.3.1
|
uses: actions/setup-dotnet@v5.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
|
|
||||||
2
.github/workflows/build-osx.yml
vendored
2
.github/workflows/build-osx.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: actions/setup-dotnet@v4.3.1
|
uses: actions/setup-dotnet@v5.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
|
|
||||||
2
.github/workflows/build-windows-desktop.yml
vendored
2
.github/workflows/build-windows-desktop.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: actions/setup-dotnet@v4.3.1
|
uses: actions/setup-dotnet@v5.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
|
|
||||||
2
.github/workflows/build-windows.yml
vendored
2
.github/workflows/build-windows.yml
vendored
|
|
@ -30,7 +30,7 @@ jobs:
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: actions/setup-dotnet@v4.3.1
|
uses: actions/setup-dotnet@v5.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,17 @@ sudo chmod 0755 "${PackagePath}/DEBIAN/postinst"
|
||||||
sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN"
|
sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN"
|
||||||
sudo chmod 0755 "${PackagePath}/opt/v2rayN/AmazTool"
|
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 dpkg-deb -Zxz --build $PackagePath
|
||||||
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb"
|
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb"
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>7.14.6</Version>
|
<Version>7.14.7</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
|
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
|
||||||
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.9" />
|
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.9" />
|
||||||
<PackageVersion Include="Semi.Avalonia.DataGrid" 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="sqlite-net-pcl" Version="1.9.172" />
|
||||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -10,23 +10,108 @@ using DynamicData.Binding;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Splat;
|
using Splat;
|
||||||
using ServiceLib.Base;
|
|
||||||
|
|
||||||
namespace ServiceLib.ViewModels;
|
namespace ServiceLib.ViewModels;
|
||||||
public class ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? updateView) : ProfilesBaseViewModel(updateView)
|
public class ProfilesSelectViewModel : MyReactiveObject
|
||||||
{
|
{
|
||||||
#region private prop
|
#region private prop
|
||||||
|
|
||||||
|
private List<ProfileItem> _lstProfile;
|
||||||
|
private string _serverFilter = string.Empty;
|
||||||
|
private Dictionary<string, bool> _dicHeaderSort = new();
|
||||||
private string _subIndexId = string.Empty;
|
private string _subIndexId = string.Empty;
|
||||||
|
// ConfigType filter state: default include-mode with all types selected
|
||||||
|
private List<EConfigType> _filterConfigTypes = new();
|
||||||
|
private bool _filterExclude = false;
|
||||||
|
|
||||||
#endregion private prop
|
#endregion private prop
|
||||||
|
|
||||||
#region Init
|
#region ObservableCollection
|
||||||
protected override async Task Initialize()
|
|
||||||
|
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;
|
get => _filterConfigTypes;
|
||||||
await base.Initialize();
|
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
|
#endregion Init
|
||||||
|
|
||||||
#region Actions
|
#region Actions
|
||||||
|
|
@ -49,6 +134,118 @@ public class ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? upd
|
||||||
|
|
||||||
#region Servers && Groups
|
#region Servers && Groups
|
||||||
|
|
||||||
|
private async Task SubSelectedChangedAsync(bool c)
|
||||||
|
{
|
||||||
|
if (!c)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_subIndexId = SelectedSub?.Id;
|
||||||
|
|
||||||
|
await RefreshServers();
|
||||||
|
|
||||||
|
await _updateView?.Invoke(EViewAction.ProfilesFocus, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ServerFilterChanged(bool c)
|
||||||
|
{
|
||||||
|
if (!c)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_serverFilter = ServerFilter;
|
||||||
|
if (_serverFilter.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
await RefreshServers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshServers()
|
||||||
|
{
|
||||||
|
await RefreshServersBiz();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshServersBiz()
|
||||||
|
{
|
||||||
|
var lstModel = await GetProfileItemsEx(_subIndexId, _serverFilter);
|
||||||
|
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
|
||||||
|
|
||||||
|
ProfileItems.Clear();
|
||||||
|
ProfileItems.AddRange(lstModel);
|
||||||
|
if (lstModel.Count > 0)
|
||||||
|
{
|
||||||
|
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
|
||||||
|
if (selected != null)
|
||||||
|
{
|
||||||
|
SelectedProfile = selected;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedProfile = lstModel.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshSubscriptions()
|
||||||
|
{
|
||||||
|
SubItems.Clear();
|
||||||
|
|
||||||
|
SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers });
|
||||||
|
|
||||||
|
foreach (var item in await AppManager.Instance.SubItems())
|
||||||
|
{
|
||||||
|
SubItems.Add(item);
|
||||||
|
}
|
||||||
|
if (_subIndexId != null && SubItems.FirstOrDefault(t => t.Id == _subIndexId) != null)
|
||||||
|
{
|
||||||
|
SelectedSub = SubItems.FirstOrDefault(t => t.Id == _subIndexId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedSub = SubItems.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
|
||||||
|
{
|
||||||
|
var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter);
|
||||||
|
|
||||||
|
//await ConfigHandler.SetDefaultServer(_config, lstModel);
|
||||||
|
|
||||||
|
lstModel = (from t in lstModel
|
||||||
|
select new ProfileItemModel
|
||||||
|
{
|
||||||
|
IndexId = t.IndexId,
|
||||||
|
ConfigType = t.ConfigType,
|
||||||
|
Remarks = t.Remarks,
|
||||||
|
Address = t.Address,
|
||||||
|
Port = t.Port,
|
||||||
|
Security = t.Security,
|
||||||
|
Network = t.Network,
|
||||||
|
StreamSecurity = t.StreamSecurity,
|
||||||
|
Subid = t.Subid,
|
||||||
|
SubRemarks = t.SubRemarks,
|
||||||
|
IsActive = t.IndexId == _config.IndexId,
|
||||||
|
}).OrderBy(t => t.Sort).ToList();
|
||||||
|
|
||||||
|
// Apply ConfigType filter (include or exclude)
|
||||||
|
if (FilterConfigTypes != null && FilterConfigTypes.Count > 0)
|
||||||
|
{
|
||||||
|
if (FilterExclude)
|
||||||
|
{
|
||||||
|
lstModel = lstModel.Where(t => !FilterConfigTypes.Contains(t.ConfigType)).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lstModel = lstModel.Where(t => FilterConfigTypes.Contains(t.ConfigType)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lstModel;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ProfileItem?> GetProfileItem()
|
public async Task<ProfileItem?> GetProfileItem()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
|
if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
|
||||||
|
|
@ -65,17 +262,44 @@ public class ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? upd
|
||||||
return item;
|
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())
|
if (colName.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var prop = typeof(ProfileItemModel).GetProperty(colName);
|
var prop = typeof(ProfileItemModel).GetProperty(colName);
|
||||||
if (prop == null)
|
if (prop == null)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_dicHeaderSort.TryAdd(colName, true);
|
_dicHeaderSort.TryAdd(colName, true);
|
||||||
|
|
@ -117,23 +341,19 @@ public class ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? upd
|
||||||
|
|
||||||
_dicHeaderSort[colName] = !asc;
|
_dicHeaderSort[colName] = !asc;
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Servers && Groups
|
#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;
|
FilterConfigTypes = types?.Distinct().ToList() ?? new List<EConfigType>();
|
||||||
}
|
FilterExclude = exclude;
|
||||||
protected override void SetCurrentSubIndexId(string? id)
|
|
||||||
{
|
|
||||||
_subIndexId = id ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool ShouldSetDefaultServer => false;
|
#endregion Public API
|
||||||
|
|
||||||
#endregion overrides
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,23 +7,40 @@ using DynamicData.Binding;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Splat;
|
using Splat;
|
||||||
using ServiceLib.Base;
|
|
||||||
|
|
||||||
namespace ServiceLib.ViewModels;
|
namespace ServiceLib.ViewModels;
|
||||||
|
|
||||||
public class ProfilesViewModel : ProfilesBaseViewModel
|
public class ProfilesViewModel : MyReactiveObject
|
||||||
{
|
{
|
||||||
#region private prop
|
#region private prop
|
||||||
|
|
||||||
|
private List<ProfileItem> _lstProfile;
|
||||||
|
private string _serverFilter = string.Empty;
|
||||||
|
private Dictionary<string, bool> _dicHeaderSort = new();
|
||||||
private SpeedtestService? _speedtestService;
|
private SpeedtestService? _speedtestService;
|
||||||
|
|
||||||
#endregion private prop
|
#endregion private prop
|
||||||
|
|
||||||
#region ObservableCollection
|
#region ObservableCollection
|
||||||
|
|
||||||
|
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
|
||||||
|
|
||||||
|
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public ProfileItemModel SelectedProfile { get; set; }
|
||||||
|
|
||||||
|
public IList<ProfileItemModel> SelectedProfiles { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public SubItem SelectedSub { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public SubItem SelectedMoveToGroup { get; set; }
|
public SubItem SelectedMoveToGroup { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public string ServerFilter { get; set; }
|
||||||
|
|
||||||
#endregion ObservableCollection
|
#endregion ObservableCollection
|
||||||
|
|
||||||
#region Menu
|
#region Menu
|
||||||
|
|
@ -72,61 +89,175 @@ public class ProfilesViewModel : ProfilesBaseViewModel
|
||||||
|
|
||||||
#region Init
|
#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
|
#region WhenAnyValue && ReactiveCommand
|
||||||
|
|
||||||
var canEditRemove = this.WhenAnyValue(
|
var canEditRemove = this.WhenAnyValue(
|
||||||
x => x.SelectedProfile,
|
x => x.SelectedProfile,
|
||||||
selectedSource => selectedSource != null && !selectedSource.IndexId.IsNullOrEmpty());
|
selectedSource => selectedSource != null && !selectedSource.IndexId.IsNullOrEmpty());
|
||||||
|
|
||||||
|
this.WhenAnyValue(
|
||||||
|
x => x.SelectedSub,
|
||||||
|
y => y != null && !y.Remarks.IsNullOrEmpty() && _config.SubIndexId != y.Id)
|
||||||
|
.Subscribe(async c => await SubSelectedChangedAsync(c));
|
||||||
this.WhenAnyValue(
|
this.WhenAnyValue(
|
||||||
x => x.SelectedMoveToGroup,
|
x => x.SelectedMoveToGroup,
|
||||||
y => y != null && !y.Remarks.IsNullOrEmpty())
|
y => y != null && !y.Remarks.IsNullOrEmpty())
|
||||||
.Subscribe(async c => await MoveToGroup(c));
|
.Subscribe(async c => await MoveToGroup(c));
|
||||||
|
|
||||||
|
this.WhenAnyValue(
|
||||||
|
x => x.ServerFilter,
|
||||||
|
y => y != null && _serverFilter != y)
|
||||||
|
.Subscribe(async c => await ServerFilterChanged(c));
|
||||||
|
|
||||||
//servers delete
|
//servers delete
|
||||||
EditServerCmd = ReactiveCommand.CreateFromTask(EditServerAsync, canEditRemove);
|
EditServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
RemoveServerCmd = ReactiveCommand.CreateFromTask(RemoveServerAsync, canEditRemove);
|
{
|
||||||
RemoveDuplicateServerCmd = ReactiveCommand.CreateFromTask(RemoveDuplicateServer);
|
await EditServerAsync(EConfigType.Custom);
|
||||||
CopyServerCmd = ReactiveCommand.CreateFromTask(CopyServer, canEditRemove);
|
}, canEditRemove);
|
||||||
SetDefaultServerCmd = ReactiveCommand.CreateFromTask(SetDefaultServer, canEditRemove);
|
RemoveServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
ShareServerCmd = ReactiveCommand.CreateFromTask(ShareServerAsync, canEditRemove);
|
{
|
||||||
SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(() =>
|
await RemoveServerAsync();
|
||||||
SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random), canEditRemove);
|
}, canEditRemove);
|
||||||
SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(() =>
|
RemoveDuplicateServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin), canEditRemove);
|
{
|
||||||
SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(() =>
|
await RemoveDuplicateServer();
|
||||||
SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing), canEditRemove);
|
});
|
||||||
SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(() =>
|
CopyServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad), canEditRemove);
|
{
|
||||||
SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(() =>
|
await CopyServer();
|
||||||
SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing), canEditRemove);
|
}, canEditRemove);
|
||||||
|
SetDefaultServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await SetDefaultServer();
|
||||||
|
}, canEditRemove);
|
||||||
|
ShareServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await ShareServerAsync();
|
||||||
|
}, canEditRemove);
|
||||||
|
SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random);
|
||||||
|
}, canEditRemove);
|
||||||
|
SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin);
|
||||||
|
}, canEditRemove);
|
||||||
|
SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing);
|
||||||
|
}, canEditRemove);
|
||||||
|
SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad);
|
||||||
|
}, canEditRemove);
|
||||||
|
SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing);
|
||||||
|
}, canEditRemove);
|
||||||
|
|
||||||
//servers move
|
//servers move
|
||||||
MoveTopCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Top), canEditRemove);
|
MoveTopCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
MoveUpCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Up), canEditRemove);
|
{
|
||||||
MoveDownCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Down), canEditRemove);
|
await MoveServer(EMove.Top);
|
||||||
MoveBottomCmd = ReactiveCommand.CreateFromTask(() => MoveServer(EMove.Bottom), canEditRemove);
|
}, canEditRemove);
|
||||||
|
MoveUpCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await MoveServer(EMove.Up);
|
||||||
|
}, canEditRemove);
|
||||||
|
MoveDownCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await MoveServer(EMove.Down);
|
||||||
|
}, canEditRemove);
|
||||||
|
MoveBottomCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await MoveServer(EMove.Bottom);
|
||||||
|
}, canEditRemove);
|
||||||
|
|
||||||
//servers ping
|
//servers ping
|
||||||
MixedTestServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Mixedtest));
|
MixedTestServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
TcpingServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Tcping), canEditRemove);
|
{
|
||||||
RealPingServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Realping), canEditRemove);
|
await ServerSpeedtest(ESpeedActionType.Mixedtest);
|
||||||
SpeedServerCmd = ReactiveCommand.CreateFromTask(() => ServerSpeedtest(ESpeedActionType.Speedtest), canEditRemove);
|
});
|
||||||
SortServerResultCmd = ReactiveCommand.CreateFromTask(() => SortServer(EServerColName.DelayVal.ToString()));
|
TcpingServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
RemoveInvalidServerResultCmd = ReactiveCommand.CreateFromTask(RemoveInvalidServerResult);
|
{
|
||||||
|
await ServerSpeedtest(ESpeedActionType.Tcping);
|
||||||
|
}, canEditRemove);
|
||||||
|
RealPingServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await ServerSpeedtest(ESpeedActionType.Realping);
|
||||||
|
}, canEditRemove);
|
||||||
|
SpeedServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await ServerSpeedtest(ESpeedActionType.Speedtest);
|
||||||
|
}, canEditRemove);
|
||||||
|
SortServerResultCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await SortServer(EServerColName.DelayVal.ToString());
|
||||||
|
});
|
||||||
|
RemoveInvalidServerResultCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await RemoveInvalidServerResult();
|
||||||
|
});
|
||||||
//servers export
|
//servers export
|
||||||
Export2ClientConfigCmd = ReactiveCommand.CreateFromTask(() => Export2ClientConfigAsync(false), canEditRemove);
|
Export2ClientConfigCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
Export2ClientConfigClipboardCmd = ReactiveCommand.CreateFromTask(() => Export2ClientConfigAsync(true), canEditRemove);
|
{
|
||||||
Export2ShareUrlCmd = ReactiveCommand.CreateFromTask(() => Export2ShareUrlAsync(false), canEditRemove);
|
await Export2ClientConfigAsync(false);
|
||||||
Export2ShareUrlBase64Cmd = ReactiveCommand.CreateFromTask(() => Export2ShareUrlAsync(true), canEditRemove);
|
}, canEditRemove);
|
||||||
|
Export2ClientConfigClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await Export2ClientConfigAsync(true);
|
||||||
|
}, canEditRemove);
|
||||||
|
Export2ShareUrlCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await Export2ShareUrlAsync(false);
|
||||||
|
}, canEditRemove);
|
||||||
|
Export2ShareUrlBase64Cmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await Export2ShareUrlAsync(true);
|
||||||
|
}, canEditRemove);
|
||||||
|
|
||||||
//Subscription
|
//Subscription
|
||||||
AddSubCmd = ReactiveCommand.CreateFromTask(() => EditSubAsync(true));
|
AddSubCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
EditSubCmd = ReactiveCommand.CreateFromTask(() => EditSubAsync(false));
|
{
|
||||||
|
await EditSubAsync(true);
|
||||||
|
});
|
||||||
|
EditSubCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await EditSubAsync(false);
|
||||||
|
});
|
||||||
|
|
||||||
#endregion WhenAnyValue && ReactiveCommand
|
#endregion WhenAnyValue && ReactiveCommand
|
||||||
|
|
||||||
|
#region AppEvents
|
||||||
|
|
||||||
|
AppEvents.ProfilesRefreshRequested
|
||||||
|
.AsObservable()
|
||||||
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.Subscribe(async _ => await RefreshServersBiz());
|
||||||
|
|
||||||
|
AppEvents.DispatcherStatisticsRequested
|
||||||
|
.AsObservable()
|
||||||
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.Subscribe(async result => await UpdateStatistics(result));
|
||||||
|
|
||||||
|
#endregion AppEvents
|
||||||
|
|
||||||
|
_ = Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Init()
|
||||||
|
{
|
||||||
|
SelectedProfile = new();
|
||||||
|
SelectedSub = new();
|
||||||
|
SelectedMoveToGroup = new();
|
||||||
|
|
||||||
|
await RefreshSubscriptions();
|
||||||
|
await RefreshServers();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Init
|
#endregion Init
|
||||||
|
|
@ -138,18 +269,18 @@ public class ProfilesViewModel : ProfilesBaseViewModel
|
||||||
Locator.Current.GetService<MainWindowViewModel>()?.Reload();
|
Locator.Current.GetService<MainWindowViewModel>()?.Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SetSpeedTestResult(SpeedTestResult result)
|
public async Task SetSpeedTestResult(SpeedTestResult result)
|
||||||
{
|
{
|
||||||
if (result.IndexId.IsNullOrEmpty())
|
if (result.IndexId.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
NoticeManager.Instance.SendMessageEx(result.Delay);
|
NoticeManager.Instance.SendMessageEx(result.Delay);
|
||||||
NoticeManager.Instance.Enqueue(result.Delay);
|
NoticeManager.Instance.Enqueue(result.Delay);
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
}
|
}
|
||||||
var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId);
|
var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.Delay.IsNotEmpty())
|
if (result.Delay.IsNotEmpty())
|
||||||
|
|
@ -163,17 +294,166 @@ public class ProfilesViewModel : ProfilesBaseViewModel
|
||||||
item.SpeedVal = result.Speed ?? string.Empty;
|
item.SpeedVal = result.Speed ?? string.Empty;
|
||||||
}
|
}
|
||||||
//_profileItems.Replace(item, JsonUtils.DeepCopy(item));
|
//_profileItems.Replace(item, JsonUtils.DeepCopy(item));
|
||||||
return Task.CompletedTask;
|
}
|
||||||
|
|
||||||
|
public async Task UpdateStatistics(ServerSpeedItem update)
|
||||||
|
{
|
||||||
|
if (!_config.GuiItem.EnableStatistics
|
||||||
|
|| (update.ProxyUp + update.ProxyDown) <= 0
|
||||||
|
|| DateTime.Now.Second % 3 != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var item = ProfileItems.FirstOrDefault(it => it.IndexId == update.IndexId);
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
item.TodayDown = Utils.HumanFy(update.TodayDown);
|
||||||
|
item.TodayUp = Utils.HumanFy(update.TodayUp);
|
||||||
|
item.TotalDown = Utils.HumanFy(update.TotalDown);
|
||||||
|
item.TotalUp = Utils.HumanFy(update.TotalUp);
|
||||||
|
|
||||||
|
//if (SelectedProfile?.IndexId == item.IndexId)
|
||||||
|
//{
|
||||||
|
// var temp = JsonUtils.DeepCopy(item);
|
||||||
|
// _profileItems.Replace(item, temp);
|
||||||
|
// SelectedProfile = temp;
|
||||||
|
//}
|
||||||
|
//else
|
||||||
|
//{
|
||||||
|
// _profileItems.Replace(item, JsonUtils.DeepCopy(item));
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Actions
|
#endregion Actions
|
||||||
|
|
||||||
protected override async Task Initialize()
|
#region Servers && Groups
|
||||||
|
|
||||||
|
private async Task SubSelectedChangedAsync(bool c)
|
||||||
{
|
{
|
||||||
await base.Initialize();
|
if (!c)
|
||||||
SelectedMoveToGroup = new();
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_config.SubIndexId = SelectedSub?.Id;
|
||||||
|
|
||||||
|
await RefreshServers();
|
||||||
|
|
||||||
|
await _updateView?.Invoke(EViewAction.ProfilesFocus, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ServerFilterChanged(bool c)
|
||||||
|
{
|
||||||
|
if (!c)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_serverFilter = ServerFilter;
|
||||||
|
if (_serverFilter.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
await RefreshServers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshServers()
|
||||||
|
{
|
||||||
|
AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default);
|
||||||
|
|
||||||
|
await Task.Delay(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshServersBiz()
|
||||||
|
{
|
||||||
|
var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter);
|
||||||
|
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
|
||||||
|
|
||||||
|
ProfileItems.Clear();
|
||||||
|
ProfileItems.AddRange(lstModel);
|
||||||
|
if (lstModel.Count > 0)
|
||||||
|
{
|
||||||
|
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
|
||||||
|
if (selected != null)
|
||||||
|
{
|
||||||
|
SelectedProfile = selected;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedProfile = lstModel.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshSubscriptions()
|
||||||
|
{
|
||||||
|
SubItems.Clear();
|
||||||
|
|
||||||
|
SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers });
|
||||||
|
|
||||||
|
foreach (var item in await AppManager.Instance.SubItems())
|
||||||
|
{
|
||||||
|
SubItems.Add(item);
|
||||||
|
}
|
||||||
|
if (_config.SubIndexId != null && SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null)
|
||||||
|
{
|
||||||
|
SelectedSub = SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedSub = SubItems.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
|
||||||
|
{
|
||||||
|
var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, filter);
|
||||||
|
|
||||||
|
await ConfigHandler.SetDefaultServer(_config, lstModel);
|
||||||
|
|
||||||
|
var lstServerStat = (_config.GuiItem.EnableStatistics ? StatisticsManager.Instance.ServerStat : null) ?? [];
|
||||||
|
var lstProfileExs = await ProfileExManager.Instance.GetProfileExs();
|
||||||
|
lstModel = (from t in lstModel
|
||||||
|
join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b
|
||||||
|
from t22 in t2b.DefaultIfEmpty()
|
||||||
|
join t3 in lstProfileExs on t.IndexId equals t3.IndexId into t3b
|
||||||
|
from t33 in t3b.DefaultIfEmpty()
|
||||||
|
select new ProfileItemModel
|
||||||
|
{
|
||||||
|
IndexId = t.IndexId,
|
||||||
|
ConfigType = t.ConfigType,
|
||||||
|
Remarks = t.Remarks,
|
||||||
|
Address = t.Address,
|
||||||
|
Port = t.Port,
|
||||||
|
Security = t.Security,
|
||||||
|
Network = t.Network,
|
||||||
|
StreamSecurity = t.StreamSecurity,
|
||||||
|
Subid = t.Subid,
|
||||||
|
SubRemarks = t.SubRemarks,
|
||||||
|
IsActive = t.IndexId == _config.IndexId,
|
||||||
|
Sort = t33?.Sort ?? 0,
|
||||||
|
Delay = t33?.Delay ?? 0,
|
||||||
|
Speed = t33?.Speed ?? 0,
|
||||||
|
DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty,
|
||||||
|
SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty,
|
||||||
|
TodayDown = t22 == null ? "" : Utils.HumanFy(t22.TodayDown),
|
||||||
|
TodayUp = t22 == null ? "" : Utils.HumanFy(t22.TodayUp),
|
||||||
|
TotalDown = t22 == null ? "" : Utils.HumanFy(t22.TotalDown),
|
||||||
|
TotalUp = t22 == null ? "" : Utils.HumanFy(t22.TotalUp)
|
||||||
|
}).OrderBy(t => t.Sort).ToList();
|
||||||
|
|
||||||
|
return lstModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Servers && Groups
|
||||||
|
|
||||||
#region Add Servers
|
#region Add Servers
|
||||||
|
|
||||||
private async Task<List<ProfileItem>?> GetProfileItems(bool latest)
|
private async Task<List<ProfileItem>?> GetProfileItems(bool latest)
|
||||||
|
|
@ -204,7 +484,7 @@ public class ProfilesViewModel : ProfilesBaseViewModel
|
||||||
return lstSelected;
|
return lstSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task EditServerAsync()
|
public async Task EditServerAsync(EConfigType eConfigType)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
|
if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
|
||||||
{
|
{
|
||||||
|
|
@ -216,7 +496,7 @@ public class ProfilesViewModel : ProfilesBaseViewModel
|
||||||
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
|
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var eConfigType = item.ConfigType;
|
eConfigType = item.ConfigType;
|
||||||
|
|
||||||
bool? ret = false;
|
bool? ret = false;
|
||||||
if (eConfigType == EConfigType.Custom)
|
if (eConfigType == EConfigType.Custom)
|
||||||
|
|
@ -237,11 +517,6 @@ public class ProfilesViewModel : ProfilesBaseViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task EditServerAsync(EConfigType _)
|
|
||||||
{
|
|
||||||
return EditServerAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemoveServerAsync()
|
public async Task RemoveServerAsync()
|
||||||
{
|
{
|
||||||
var lstSelected = await GetProfileItems(true);
|
var lstSelected = await GetProfileItems(true);
|
||||||
|
|
@ -376,7 +651,7 @@ public class ProfilesViewModel : ProfilesBaseViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
_dicHeaderSort.TryAdd(colName, true);
|
_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)
|
if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
@ -458,14 +733,13 @@ public class ProfilesViewModel : ProfilesBaseViewModel
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_speedtestService ??= new SpeedtestService(_config, (SpeedTestResult result) =>
|
_speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) =>
|
||||||
{
|
{
|
||||||
RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) =>
|
RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) =>
|
||||||
{
|
{
|
||||||
_ = SetSpeedTestResult(result);
|
_ = SetSpeedTestResult(result);
|
||||||
return Disposable.Empty;
|
return Disposable.Empty;
|
||||||
});
|
});
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
});
|
||||||
_speedtestService?.RunLoop(actionType, lstSelected);
|
_speedtestService?.RunLoop(actionType, lstSelected);
|
||||||
}
|
}
|
||||||
|
|
@ -579,27 +853,4 @@ public class ProfilesViewModel : ProfilesBaseViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Subscription
|
#endregion Subscription
|
||||||
|
|
||||||
#region overrides
|
|
||||||
|
|
||||||
protected override string GetCurrentSubIndexId()
|
|
||||||
{
|
|
||||||
return _config.SubIndexId;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SetCurrentSubIndexId(string? id)
|
|
||||||
{
|
|
||||||
_config.SubIndexId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task RefreshServers()
|
|
||||||
{
|
|
||||||
AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default);
|
|
||||||
await Task.Delay(200);
|
|
||||||
await base.RefreshServers();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool ShouldSetDefaultServer => true;
|
|
||||||
|
|
||||||
#endregion overrides
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
x:Class="v2rayN.Desktop.Views.ProfilesSelectWindow"
|
x:Class="v2rayN.Desktop.Views.ProfilesSelectWindow"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:conv="using:v2rayN.Desktop.Converters"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
|
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
|
||||||
|
|
@ -11,12 +10,9 @@
|
||||||
Width="800"
|
Width="800"
|
||||||
Height="450"
|
Height="450"
|
||||||
x:DataType="vms:ProfilesSelectViewModel"
|
x:DataType="vms:ProfilesSelectViewModel"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<Window.Resources>
|
|
||||||
<conv:DelayColorConverter x:Key="DelayColorConverter" />
|
|
||||||
</Window.Resources>
|
|
||||||
|
|
||||||
<DockPanel Margin="8">
|
<DockPanel Margin="8">
|
||||||
<!-- Bottom buttons -->
|
<!-- Bottom buttons -->
|
||||||
<StackPanel
|
<StackPanel
|
||||||
|
|
@ -94,7 +90,7 @@
|
||||||
Header="{x:Static resx:ResUI.LvServiceType}"
|
Header="{x:Static resx:ResUI.LvServiceType}"
|
||||||
Tag="ConfigType" />
|
Tag="ConfigType" />
|
||||||
|
|
||||||
<DataGridTemplateColumn Tag="Remarks">
|
<DataGridTemplateColumn SortMemberPath="Remarks" Tag="Remarks">
|
||||||
<DataGridTemplateColumn.Header>
|
<DataGridTemplateColumn.Header>
|
||||||
<TextBlock Text="{x:Static resx:ResUI.LvRemarks}" />
|
<TextBlock Text="{x:Static resx:ResUI.LvRemarks}" />
|
||||||
</DataGridTemplateColumn.Header>
|
</DataGridTemplateColumn.Header>
|
||||||
|
|
@ -132,49 +128,6 @@
|
||||||
Binding="{Binding SubRemarks}"
|
Binding="{Binding SubRemarks}"
|
||||||
Header="{x:Static resx:ResUI.LvSubscription}"
|
Header="{x:Static resx:ResUI.LvSubscription}"
|
||||||
Tag="SubRemarks" />
|
Tag="SubRemarks" />
|
||||||
|
|
||||||
<DataGridTemplateColumn SortMemberPath="Delay" Tag="DelayVal">
|
|
||||||
<DataGridTemplateColumn.Header>
|
|
||||||
<TextBlock Text="{x:Static resx:ResUI.LvTestDelay}" />
|
|
||||||
</DataGridTemplateColumn.Header>
|
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<TextBlock
|
|
||||||
Margin="8,0"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Foreground="{Binding Delay, Converter={StaticResource DelayColorConverter}}"
|
|
||||||
Text="{Binding DelayVal}" />
|
|
||||||
</DataTemplate>
|
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
|
||||||
</DataGridTemplateColumn>
|
|
||||||
|
|
||||||
<DataGridTextColumn
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding SpeedVal}"
|
|
||||||
Header="{x:Static resx:ResUI.LvTestSpeed}"
|
|
||||||
Tag="SpeedVal" />
|
|
||||||
|
|
||||||
<DataGridTextColumn
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding TodayUp}"
|
|
||||||
Header="{x:Static resx:ResUI.LvTodayUploadDataAmount}"
|
|
||||||
Tag="TodayUp" />
|
|
||||||
<DataGridTextColumn
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding TodayDown}"
|
|
||||||
Header="{x:Static resx:ResUI.LvTodayDownloadDataAmount}"
|
|
||||||
Tag="TodayDown" />
|
|
||||||
<DataGridTextColumn
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding TotalUp}"
|
|
||||||
Header="{x:Static resx:ResUI.LvTotalUploadDataAmount}"
|
|
||||||
Tag="TotalUp" />
|
|
||||||
<DataGridTextColumn
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding TotalDown}"
|
|
||||||
Header="{x:Static resx:ResUI.LvTotalDownloadDataAmount}"
|
|
||||||
Tag="TotalDown" />
|
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ using System.Reactive.Disposables;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ServiceLib.Manager;
|
using ServiceLib.Manager;
|
||||||
|
|
@ -17,7 +19,8 @@ public partial class ProfilesSelectWindow : ReactiveWindow<ProfilesSelectViewMod
|
||||||
{
|
{
|
||||||
private static Config _config;
|
private static Config _config;
|
||||||
|
|
||||||
public Task<ProfileItem?> ProfileItem => GetFirstProfileItemAsync();
|
public Task<ProfileItem?> ProfileItem => GetProfileItem();
|
||||||
|
public Task<List<ProfileItem>?> ProfileItems => GetProfileItems();
|
||||||
private bool _allowMultiSelect = false;
|
private bool _allowMultiSelect = false;
|
||||||
|
|
||||||
public ProfilesSelectWindow()
|
public ProfilesSelectWindow()
|
||||||
|
|
@ -46,7 +49,6 @@ public partial class ProfilesSelectWindow : ReactiveWindow<ProfilesSelectViewMod
|
||||||
this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
|
|
||||||
btnSave.Click += (s, e) => ViewModel?.SelectFinish();
|
|
||||||
btnCancel.Click += (s, e) => Close(false);
|
btnCancel.Click += (s, e) => Close(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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)
|
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
||||||
{
|
{
|
||||||
switch (action)
|
switch (action)
|
||||||
|
|
@ -96,17 +102,32 @@ public partial class ProfilesSelectWindow : ReactiveWindow<ProfilesSelectViewMod
|
||||||
|
|
||||||
private void LstProfiles_DoubleTapped(object? sender, TappedEventArgs e)
|
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;
|
e.Handled = true;
|
||||||
if (ViewModel != null && e.Column?.Tag?.ToString() != null)
|
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)
|
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();
|
var item = await ViewModel?.GetProfileItem();
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<ProfileItem>?> GetProfileItems()
|
||||||
|
{
|
||||||
|
var item = await ViewModel?.GetProfileItems();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
private void BtnSave_Click(object sender, RoutedEventArgs e)
|
private void BtnSave_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Trigger selection finalize when Confirm is clicked
|
// Trigger selection finalize when Confirm is clicked
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
|
||||||
private async void BtnSelectProfile_Click(object? sender, RoutedEventArgs e)
|
private async void BtnSelectProfile_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var selectWindow = new ProfilesSelectWindow();
|
var selectWindow = new ProfilesSelectWindow();
|
||||||
|
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
|
||||||
var result = await selectWindow.ShowDialog<bool?>(this);
|
var result = await selectWindow.ShowDialog<bool?>(this);
|
||||||
if (result == true)
|
if (result == true)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
||||||
private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e)
|
private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var selectWindow = new ProfilesSelectWindow();
|
var selectWindow = new ProfilesSelectWindow();
|
||||||
|
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
|
||||||
var result = await selectWindow.ShowDialog<bool?>(this);
|
var result = await selectWindow.ShowDialog<bool?>(this);
|
||||||
if (result == true)
|
if (result == true)
|
||||||
{
|
{
|
||||||
|
|
@ -78,6 +79,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
||||||
private async void BtnSelectNextProfile_Click(object? sender, RoutedEventArgs e)
|
private async void BtnSelectNextProfile_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var selectWindow = new ProfilesSelectWindow();
|
var selectWindow = new ProfilesSelectWindow();
|
||||||
|
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
|
||||||
var result = await selectWindow.ShowDialog<bool?>(this);
|
var result = await selectWindow.ShowDialog<bool?>(this);
|
||||||
if (result == true)
|
if (result == true)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:base="clr-namespace:v2rayN.Base"
|
xmlns:base="clr-namespace:v2rayN.Base"
|
||||||
xmlns:conv="clr-namespace:v2rayN.Converters"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
|
@ -15,15 +14,9 @@
|
||||||
Height="450"
|
Height="450"
|
||||||
x:TypeArguments="vms:ProfilesSelectViewModel"
|
x:TypeArguments="vms:ProfilesSelectViewModel"
|
||||||
Style="{StaticResource WindowGlobal}"
|
Style="{StaticResource WindowGlobal}"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<Window.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" />
|
|
||||||
<conv:DelayColorConverter x:Key="DelayColorConverter" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Window.Resources>
|
|
||||||
|
|
||||||
<DockPanel Margin="{StaticResource Margin8}">
|
<DockPanel Margin="{StaticResource Margin8}">
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
|
|
@ -33,9 +26,9 @@
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
Width="100"
|
||||||
|
Click="BtnSave_Click"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True"
|
IsDefault="True"
|
||||||
Click="BtnSave_Click"
|
|
||||||
Style="{StaticResource DefButton}" />
|
Style="{StaticResource DefButton}" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
|
|
@ -155,55 +148,6 @@
|
||||||
Binding="{Binding SubRemarks}"
|
Binding="{Binding SubRemarks}"
|
||||||
ExName="SubRemarks"
|
ExName="SubRemarks"
|
||||||
Header="{x:Static resx:ResUI.LvSubscription}" />
|
Header="{x:Static resx:ResUI.LvSubscription}" />
|
||||||
<base:MyDGTextColumn
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding DelayVal}"
|
|
||||||
ExName="DelayVal"
|
|
||||||
Header="{x:Static resx:ResUI.LvTestDelay}"
|
|
||||||
SortMemberPath="Delay">
|
|
||||||
<DataGridTextColumn.ElementStyle>
|
|
||||||
<Style TargetType="{x:Type TextBlock}">
|
|
||||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
|
||||||
<Setter Property="Foreground" Value="{Binding Delay, Converter={StaticResource DelayColorConverter}}" />
|
|
||||||
</Style>
|
|
||||||
</DataGridTextColumn.ElementStyle>
|
|
||||||
</base:MyDGTextColumn>
|
|
||||||
<base:MyDGTextColumn
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding SpeedVal}"
|
|
||||||
ExName="SpeedVal"
|
|
||||||
Header="{x:Static resx:ResUI.LvTestSpeed}">
|
|
||||||
<DataGridTextColumn.ElementStyle>
|
|
||||||
<Style TargetType="{x:Type TextBlock}">
|
|
||||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
|
||||||
</Style>
|
|
||||||
</DataGridTextColumn.ElementStyle>
|
|
||||||
</base:MyDGTextColumn>
|
|
||||||
|
|
||||||
<base:MyDGTextColumn
|
|
||||||
x:Name="colTodayUp"
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding TodayUp}"
|
|
||||||
ExName="TodayUp"
|
|
||||||
Header="{x:Static resx:ResUI.LvTodayUploadDataAmount}" />
|
|
||||||
<base:MyDGTextColumn
|
|
||||||
x:Name="colTodayDown"
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding TodayDown}"
|
|
||||||
ExName="TodayDown"
|
|
||||||
Header="{x:Static resx:ResUI.LvTodayDownloadDataAmount}" />
|
|
||||||
<base:MyDGTextColumn
|
|
||||||
x:Name="colTotalUp"
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding TotalUp}"
|
|
||||||
ExName="TotalUp"
|
|
||||||
Header="{x:Static resx:ResUI.LvTotalUploadDataAmount}" />
|
|
||||||
<base:MyDGTextColumn
|
|
||||||
x:Name="colTotalDown"
|
|
||||||
Width="100"
|
|
||||||
Binding="{Binding TotalDown}"
|
|
||||||
ExName="TotalDown"
|
|
||||||
Header="{x:Static resx:ResUI.LvTotalDownloadDataAmount}" />
|
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ public partial class ProfilesSelectWindow
|
||||||
{
|
{
|
||||||
private static Config _config;
|
private static Config _config;
|
||||||
|
|
||||||
public Task<ProfileItem?> ProfileItem => GetFirstProfileItemAsync();
|
public Task<ProfileItem?> ProfileItem => GetProfileItem();
|
||||||
|
public Task<List<ProfileItem>?> ProfileItems => GetProfileItems();
|
||||||
private bool _allowMultiSelect = false;
|
private bool _allowMultiSelect = false;
|
||||||
|
|
||||||
public ProfilesSelectWindow()
|
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
|
#region Event
|
||||||
|
|
||||||
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
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();
|
var item = await ViewModel?.GetProfileItem();
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<ProfileItem>?> GetProfileItems()
|
||||||
|
{
|
||||||
|
var item = await ViewModel?.GetProfileItems();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
private void BtnSave_Click(object sender, RoutedEventArgs e)
|
private void BtnSave_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Trigger selection finalize when Confirm is clicked
|
// Trigger selection finalize when Confirm is clicked
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ public partial class RoutingRuleDetailsWindow
|
||||||
private async void BtnSelectProfile_Click(object sender, RoutedEventArgs e)
|
private async void BtnSelectProfile_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var selectWindow = new ProfilesSelectWindow();
|
var selectWindow = new ProfilesSelectWindow();
|
||||||
|
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
|
||||||
if (selectWindow.ShowDialog() == true)
|
if (selectWindow.ShowDialog() == true)
|
||||||
{
|
{
|
||||||
var profile = await selectWindow.ProfileItem;
|
var profile = await selectWindow.ProfileItem;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ public partial class SubEditWindow
|
||||||
private async void BtnSelectPrevProfile_Click(object sender, RoutedEventArgs e)
|
private async void BtnSelectPrevProfile_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var selectWindow = new ProfilesSelectWindow();
|
var selectWindow = new ProfilesSelectWindow();
|
||||||
|
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
|
||||||
if (selectWindow.ShowDialog() == true)
|
if (selectWindow.ShowDialog() == true)
|
||||||
{
|
{
|
||||||
var profile = await selectWindow.ProfileItem;
|
var profile = await selectWindow.ProfileItem;
|
||||||
|
|
@ -71,6 +72,7 @@ public partial class SubEditWindow
|
||||||
private async void BtnSelectNextProfile_Click(object sender, RoutedEventArgs e)
|
private async void BtnSelectNextProfile_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var selectWindow = new ProfilesSelectWindow();
|
var selectWindow = new ProfilesSelectWindow();
|
||||||
|
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
|
||||||
if (selectWindow.ShowDialog() == true)
|
if (selectWindow.ShowDialog() == true)
|
||||||
{
|
{
|
||||||
var profile = await selectWindow.ProfileItem;
|
var profile = await selectWindow.ProfileItem;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue