Adjust Clash API (#10088)

This commit is contained in:
DHR60
2026-09-03 12:21:46 +00:00
committed by GitHub
parent f03db020fa
commit 093b8d4c34
25 changed files with 440 additions and 515 deletions

View File

@@ -2,7 +2,14 @@ namespace ServiceLib.Base;
public class BulkObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification = false;
private bool _suppressNotification;
public BulkObservableCollection()
{
}
public BulkObservableCollection(IEnumerable<T> collection) : base(collection) { }
public BulkObservableCollection(List<T> list) : base(list) { }
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
@@ -63,4 +70,29 @@ public class BulkObservableCollection<T> : ObservableCollection<T>
return true;
}
public void ReplaceRange(IEnumerable<T>? collection)
{
if (collection == null)
{
return;
}
_suppressNotification = true;
try
{
Items.Clear();
foreach (var item in collection)
{
Items.Add(item);
}
}
finally
{
_suppressNotification = false;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
}

View File

@@ -5,5 +5,4 @@ public enum ERuleMode
Rule = 0,
Global = 1,
Direct = 2,
Unchanged = 3
}

View File

@@ -8,6 +8,7 @@ global using System.Net.NetworkInformation;
global using System.Net.Sockets;
global using ReactiveUI.Primitives;
global using ReactiveUI.Primitives.Concurrency;
global using ReactiveUI.Primitives.Extensions;
global using ReactiveUI.Primitives.Disposables;
global using ReactiveUI.Primitives.Signals;
global using System.Reflection;
@@ -41,5 +42,3 @@ global using ServiceLib.Services;
global using ServiceLib.Services.CoreConfig;
global using ServiceLib.Services.Statistics;
global using SQLite;

View File

@@ -1,113 +1,104 @@
using static ServiceLib.Models.Dto.ClashProxies;
namespace ServiceLib.Manager;
public sealed class ClashApiManager
{
private const string _tag = "ClashApiManager";
private static readonly Lazy<ClashApiManager> instance = new(() => new());
public static ClashApiManager Instance => instance.Value;
private static readonly string _tag = "ClashApiHandler";
private Dictionary<string, ProxiesItem>? _proxies;
public Dictionary<string, object> ProfileContent { get; set; }
private static string ApiUrl => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort2}";
public async Task<Tuple<ClashProxies, ClashProviders>?> GetClashProxiesAsync()
public async Task<ClashItem?> GetProxies()
{
for (var i = 0; i < 3; i++)
{
var url = $"{GetApiUrl()}/proxies";
var result = await HttpClientHelper.Instance.TryGetAsync(url);
var clashProxies = JsonUtils.Deserialize<ClashProxies>(result);
var url = $"{ApiUrl}/proxies";
var resultTask = HttpClientHelper.Instance.TryGetAsync(url);
var url2 = $"{GetApiUrl()}/providers/proxies";
var result2 = await HttpClientHelper.Instance.TryGetAsync(url2);
var url2 = $"{ApiUrl}/providers/proxies";
var result2Task = HttpClientHelper.Instance.TryGetAsync(url2);
await Task.WhenAll(resultTask, result2Task);
var result = await resultTask;
var result2 = await result2Task;
var clashProxies = JsonUtils.Deserialize<ClashProxies>(result);
var clashProviders = JsonUtils.Deserialize<ClashProviders>(result2);
if (clashProxies != null || clashProviders != null)
if (clashProxies is null && clashProviders is null)
{
_proxies = clashProxies?.proxies;
return new Tuple<ClashProxies, ClashProviders>(clashProxies, clashProviders);
await Task.Delay(2000);
continue;
}
await Task.Delay(2000);
var item = new ClashItem();
if (clashProxies?.proxies is { Count: > 0 } proxies)
{
item = item with { Proxies = proxies };
}
if (clashProviders.providers is { Count: > 0 } providers)
{
foreach (var provider in providers)
{
foreach (var proxy in provider.Value.proxies ?? [])
{
if (string.IsNullOrEmpty(proxy.name)
|| !item.Proxies.TryAdd(proxy.name, proxy))
{
continue;
}
item.ProviderIndexMap.Add(proxy.name, provider.Key);
}
}
}
return item;
}
return null;
}
public void ClashProxiesDelayTest(bool blAll, List<ClashProxyModel> lstProxy, Func<ClashProxyModel?, string, Task> updateFunc)
public async Task<int> TestDelay(string name, ClashItem? clashItem)
{
Task.Run(async () =>
if (clashItem?.ProviderIndexMap.TryGetValue(name, out var providerName) == true)
{
if (blAll)
{
if (_proxies == null)
{
await GetClashProxiesAsync();
}
lstProxy = [];
lstProxy.AddRange(from kv in _proxies ?? []
where !Global.notAllowTestType.Contains(kv.Value.type?.ToLower())
select new ClashProxyModel()
{
Name = kv.Value.name,
Type = kv.Value.type?.ToLower(),
});
}
if (lstProxy is not { Count: > 0 })
{
return;
}
var urlBase = $"{GetApiUrl()}/proxies";
urlBase += @"/{0}/delay?timeout=10000&url=" + AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var tasks = new List<Task>();
foreach (var it in lstProxy)
{
if (Global.notAllowTestType.Contains(it.Type.ToLower()))
{
continue;
}
var name = it.Name;
var url = string.Format(urlBase, name);
tasks.Add(Task.Run(async () =>
{
var result = await HttpClientHelper.Instance.TryGetAsync(url);
await updateFunc?.Invoke(it, result);
}));
}
await Task.WhenAll(tasks);
await Task.Delay(1000);
await updateFunc?.Invoke(null, "");
});
return await TestProviderProxyDelay(name, providerName);
}
return await TestProxyDelay(name);
}
public List<ProxiesItem>? GetClashProxyGroups()
private async Task<int> TestProxyDelay(string name)
{
var url = $"{ApiUrl}/proxies/{Utils.UrlEncode(name)}/delay?timeout=10000&url=" +
Utils.UrlEncode(AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl);
return await SendTestRequest(url);
}
private async Task<int> TestProviderProxyDelay(string name, string providerName)
{
var url =
$"{ApiUrl}/providers/proxies/{Utils.UrlEncode(providerName)}/{Utils.UrlEncode(name)}/healthcheck?timeout=10000&url=" +
Utils.UrlEncode(AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl);
return await SendTestRequest(url);
}
private async Task<int> SendTestRequest(string url)
{
var result = await HttpClientHelper.Instance.TryGetAsync(url);
var jsonObject = JsonUtils.ParseJson(result) as JsonObject;
return jsonObject?["delay"] is { } n && n.GetValueKind() == JsonValueKind.Number &&
n.GetValue<JsonElement>().TryGetInt32(out var v)
? v
: -1;
}
public async Task SetActiveProxy(string groupName, string nodeName)
{
try
{
var fileContent = ProfileContent;
if (fileContent is null || fileContent?.ContainsKey("proxy-groups") == false)
{
return null;
}
return JsonUtils.Deserialize<List<ProxiesItem>>(JsonUtils.Serialize(fileContent["proxy-groups"]));
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return null;
}
}
public async Task ClashSetActiveProxy(string name, string nameNode)
{
try
{
var url = $"{GetApiUrl()}/proxies/{name}";
var url = $"{ApiUrl}/proxies/{Utils.UrlEncode(groupName)}";
var headers = new Dictionary<string, string>();
headers.Add("name", nameNode);
headers.Add("name", nodeName);
await HttpClientHelper.Instance.PutAsync(url, headers);
}
catch (Exception ex)
@@ -116,39 +107,67 @@ public sealed class ClashApiManager
}
}
public async Task ClashConfigUpdate(Dictionary<string, string> headers)
public async Task UpdateClashMode(string mode)
{
if (_proxies == null)
var headers = new Dictionary<string, string>
{
return;
}
{ "mode", mode },
};
await UpdateConfig(headers);
}
var urlBase = $"{GetApiUrl()}/configs";
public async Task UpdateConfig(Dictionary<string, string> headers)
{
var urlBase = $"{ApiUrl}/configs";
await HttpClientHelper.Instance.PatchAsync(urlBase, headers);
}
public async Task ClashConfigReload(string filePath)
public async Task<List<string>> GetClashModes()
{
await ClashConnectionClose("");
try
var jsonNode = await GetConfig();
if ((jsonNode?["mode-list"] ?? jsonNode?["modes"]) is not JsonArray { Count: > 0 } modesArray)
{
var url = $"{GetApiUrl()}/configs?force=true";
var headers = new Dictionary<string, string>();
headers.Add("path", filePath);
await HttpClientHelper.Instance.PutAsync(url, headers);
return [];
}
catch (Exception ex)
var modes = new List<string>();
foreach (var mode in modesArray)
{
Logging.SaveLog(_tag, ex);
if (mode is JsonValue jsonValue && jsonValue.GetValueKind() == JsonValueKind.String)
{
modes.Add(jsonValue.GetValue<string>());
}
}
return modes;
}
public async Task<ClashConnections?> GetClashConnectionsAsync()
public async Task<string?> GetClashMode()
{
var jsonNode = await GetConfig();
if (jsonNode["mode"] is not JsonValue jsonValue || jsonValue.GetValueKind() != JsonValueKind.String)
{
return null;
}
return jsonValue.GetValue<string>();
}
public async Task<JsonObject> GetConfig()
{
var url = $"{ApiUrl}/configs";
var result = await HttpClientHelper.Instance.TryGetAsync(url);
var jsonNode = JsonUtils.ParseJson(result);
if (jsonNode is not JsonObject jsonObject)
{
return new JsonObject();
}
return jsonObject;
}
public async Task<ClashConnections?> GetConnections()
{
try
{
var url = $"{GetApiUrl()}/connections";
var url = $"{ApiUrl}/connections";
var result = await HttpClientHelper.Instance.TryGetAsync(url);
var clashConnections = JsonUtils.Deserialize<ClashConnections>(result);
@@ -162,11 +181,11 @@ public sealed class ClashApiManager
return null;
}
public async Task ClashConnectionClose(string id)
public async Task CloseConnection(string id)
{
try
{
var url = $"{GetApiUrl()}/connections/{id}";
var url = $"{ApiUrl}/connections/{id}";
await HttpClientHelper.Instance.DeleteAsync(url);
}
catch (Exception ex)
@@ -174,9 +193,4 @@ public sealed class ClashApiManager
Logging.SaveLog(_tag, ex);
}
}
private string GetApiUrl()
{
return $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort2}";
}
}

View File

@@ -209,7 +209,6 @@ public class HysteriaItem
[Serializable]
public class ClashUIItem
{
public ERuleMode RuleMode { get; set; }
public bool EnableIPv6 { get; set; }
public bool EnableMixinContent { get; set; }
public int ProxiesSorting { get; set; }

View File

@@ -0,0 +1,44 @@
namespace ServiceLib.Models.Dto;
public record ClashItem
{
public Dictionary<string, ClashProxy> Proxies { get; init; } = [];
public Dictionary<string, string> ProviderIndexMap { get; init; } = [];
public bool IsEmpty() => Proxies.Count == 0;
}
public record ClashProxy
{
public List<string>? all { get; init; }
public List<HistoryItem>? history { get; init; }
public string? name { get; init; }
public string? type { get; init; }
public bool udp { get; init; }
public string? now { get; init; }
public int delay { get; init; }
public record HistoryItem
{
public string? time { get; init; }
public int delay { get; init; }
}
}
public record ClashProvider
{
public string? name { get; init; }
public List<ClashProxy>? proxies { get; init; }
public string? type { get; init; }
public string? vehicleType { get; init; }
}
public record ClashProxies
{
public Dictionary<string, ClashProxy> proxies { get; init; } = [];
}
public record ClashProviders
{
public Dictionary<string, ClashProvider> providers { get; init; } = [];
}

View File

@@ -1,16 +0,0 @@
using static ServiceLib.Models.Dto.ClashProxies;
namespace ServiceLib.Models.Dto;
public class ClashProviders
{
public Dictionary<string, ProvidersItem>? providers { get; set; }
public class ProvidersItem
{
public string? name { get; set; }
public List<ProxiesItem>? proxies { get; set; }
public string? type { get; set; }
public string? vehicleType { get; set; }
}
}

View File

@@ -1,23 +0,0 @@
namespace ServiceLib.Models.Dto;
public class ClashProxies
{
public Dictionary<string, ProxiesItem>? proxies { get; set; }
public class ProxiesItem
{
public List<string>? all { get; set; }
public List<HistoryItem>? history { get; set; }
public string? name { get; set; }
public string? type { get; set; }
public bool udp { get; set; }
public string? now { get; set; }
public int delay { get; set; }
}
public class HistoryItem
{
public string? time { get; set; }
public int delay { get; set; }
}
}

View File

@@ -3,9 +3,9 @@ namespace ServiceLib.Models.Dto;
[Serializable]
public partial class ClashProxyModel : ReactiveObject
{
public string? Name { get; set; }
public required string Name { get; set; }
public string? Type { get; set; }
public required string Type { get; set; }
public string? Now { get; set; }

View File

@@ -987,6 +987,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Latency Test 的本地化字符串。
/// </summary>
public static string menuDelaytest {
get {
return ResourceManager.GetString("menuDelaytest", resourceCulture);
}
}
/// <summary>
/// 查找类似 DNS Settings 的本地化字符串。
/// </summary>
@@ -1401,15 +1410,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Latency Test 的本地化字符串。
/// </summary>
public static string menuProxiesDelaytest {
get {
return ResourceManager.GetString("menuProxiesDelaytest", resourceCulture);
}
}
/// <summary>
/// 查找类似 Part Node Latency Test 的本地化字符串。
/// </summary>

View File

@@ -1215,7 +1215,7 @@
<data name="menuModeRule" xml:space="preserve">
<value>قانون</value>
</data>
<data name="menuProxiesDelaytest" xml:space="preserve">
<data name="menuDelaytest" xml:space="preserve">
<value>تست تأخیر</value>
</data>
<data name="menuProxiesDelaytestPart" xml:space="preserve">

View File

@@ -1212,7 +1212,7 @@
<data name="menuModeRule" xml:space="preserve">
<value>Règle</value>
</data>
<data name="menuProxiesDelaytest" xml:space="preserve">
<data name="menuDelaytest" xml:space="preserve">
<value>Test de latence</value>
</data>
<data name="menuProxiesDelaytestPart" xml:space="preserve">

View File

@@ -1215,7 +1215,7 @@
<data name="menuModeRule" xml:space="preserve">
<value>Szabály</value>
</data>
<data name="menuProxiesDelaytest" xml:space="preserve">
<data name="menuDelaytest" xml:space="preserve">
<value>Késleltetés teszt</value>
</data>
<data name="menuProxiesDelaytestPart" xml:space="preserve">

View File

@@ -1215,7 +1215,7 @@
<data name="menuModeRule" xml:space="preserve">
<value>Aturan</value>
</data>
<data name="menuProxiesDelaytest" xml:space="preserve">
<data name="menuDelaytest" xml:space="preserve">
<value>Uji latensi</value>
</data>
<data name="menuProxiesDelaytestPart" xml:space="preserve">

View File

@@ -1221,7 +1221,7 @@
<data name="menuModeRule" xml:space="preserve">
<value>Rule</value>
</data>
<data name="menuProxiesDelaytest" xml:space="preserve">
<data name="menuDelaytest" xml:space="preserve">
<value>Latency Test</value>
</data>
<data name="menuProxiesDelaytestPart" xml:space="preserve">

View File

@@ -1221,7 +1221,7 @@
<data name="menuModeRule" xml:space="preserve">
<value>Правила</value>
</data>
<data name="menuProxiesDelaytest" xml:space="preserve">
<data name="menuDelaytest" xml:space="preserve">
<value>Тест задержки</value>
</data>
<data name="menuProxiesDelaytestPart" xml:space="preserve">

View File

@@ -1218,7 +1218,7 @@
<data name="menuModeRule" xml:space="preserve">
<value>规则</value>
</data>
<data name="menuProxiesDelaytest" xml:space="preserve">
<data name="menuDelaytest" xml:space="preserve">
<value>延迟测试</value>
</data>
<data name="menuProxiesDelaytestPart" xml:space="preserve">

View File

@@ -1218,7 +1218,7 @@
<data name="menuModeRule" xml:space="preserve">
<value>規則</value>
</data>
<data name="menuProxiesDelaytest" xml:space="preserve">
<data name="menuDelaytest" xml:space="preserve">
<value>延遲測試</value>
</data>
<data name="menuProxiesDelaytestPart" xml:space="preserve">

View File

@@ -94,17 +94,7 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
//mode
if (!fileContent.ContainsKey("mode"))
{
fileContent["mode"] = nameof(ERuleMode.Rule).ToLower();
}
else
{
if (config.ClashUIItem.RuleMode != ERuleMode.Unchanged)
{
fileContent["mode"] = config.ClashUIItem.RuleMode.ToString().ToLower();
}
}
fileContent.TryAdd("mode", nameof(ERuleMode.Rule));
//enable tun mode
if (isTunEnabled)
@@ -159,8 +149,6 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
return ret;
}
ClashApiManager.Instance.ProfileContent = fileContent;
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}");
ret.Success = true;
return ret;

View File

@@ -2,33 +2,18 @@ namespace ServiceLib.ViewModels;
public partial class ClashConnectionsViewModel : MyReactiveObject
{
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
[Reactive]
public partial ClashConnectionModel SelectedSource { get; set; }
public ReactiveCommand<RxVoid, RxVoid> ConnectionCloseCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> ConnectionCloseAllCmd { get; }
[Reactive]
public partial string HostFilter { get; set; }
[Reactive]
public partial bool AutoRefresh { get; set; }
public ClashConnectionsViewModel()
{
_config = AppManager.Instance.Config;
AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh;
var canEditRemove = this.WhenAnyValue(
x => x.SelectedSource,
selectedSource => selectedSource != null && selectedSource.Id.IsNotEmpty());
x => x.SelectedSource,
selectedSource => selectedSource?.Id?.IsNotEmpty() == true);
this.WhenAnyValue(
x => x.AutoRefresh,
y => y == true)
.Subscribe(c => { _config.ClashUIItem.ConnectionsAutoRefresh = AutoRefresh; });
x => x.AutoRefresh)
.Subscribe(_ => { _config.ClashUIItem.ConnectionsAutoRefresh = AutoRefresh; });
ConnectionCloseCmd = ReactiveCommand.CreateFromTask(async () =>
{
await ClashConnectionClose(false);
@@ -39,17 +24,28 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await ClashConnectionClose(true);
});
_ = Init();
_ = Task.Factory.StartNew(
async () => await GetClashConnectionsTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
}
private async Task Init()
{
await DelayTestTask();
}
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
[Reactive] public partial ClashConnectionModel SelectedSource { get; set; }
public ReactiveCommand<RxVoid, RxVoid> ConnectionCloseCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> ConnectionCloseAllCmd { get; }
[Reactive] public partial string HostFilter { get; set; }
[Reactive] public partial bool AutoRefresh { get; set; }
private async Task GetClashConnections()
{
var ret = await ClashApiManager.Instance.GetClashConnectionsAsync();
var ret = await ClashApiManager.Instance.GetConnections();
if (ret == null)
{
return;
@@ -69,7 +65,8 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
var lstModel = new List<ClashConnectionModel>();
foreach (var item in connections ?? [])
{
var host = $"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}";
var host =
$"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}";
if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter))
{
continue;
@@ -83,7 +80,7 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
Host = host,
Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds,
Elapsed = (dtNow - item.start).ToString(@"hh\:mm\:ss"),
Chain = $"{item.rule} , {string.Join("->", item.chains ?? [])}"
Chain = $"{item.rule} , {string.Join("->", item.chains ?? [])}",
};
lstModel.Add(model);
@@ -103,47 +100,39 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
if (!all)
{
var item = SelectedSource;
if (item is null)
if (string.IsNullOrEmpty(item?.Id))
{
return;
}
id = item.Id;
}
else
{
ConnectionItems.Clear();
}
await ClashApiManager.Instance.ClashConnectionClose(id);
await ClashApiManager.Instance.CloseConnection(id);
await GetClashConnections();
}
public async Task DelayTestTask()
public async Task GetClashConnectionsTask()
{
_ = Task.Run(async () =>
var numOfExecuted = 1;
while (true)
{
var numOfExecuted = 1;
while (true)
await Task.Delay(1000 * 5);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
await Task.Delay(1000 * 5);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{
continue;
}
await GetClashConnections();
continue;
}
});
await Task.CompletedTask;
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{
continue;
}
await GetClashConnections();
}
}
}

View File

@@ -1,36 +1,9 @@
using static ServiceLib.Models.Dto.ClashProviders;
using static ServiceLib.Models.Dto.ClashProxies;
namespace ServiceLib.ViewModels;
public partial class ClashProxiesViewModel : MyReactiveObject
{
private Dictionary<string, ProxiesItem>? _proxies;
private Dictionary<string, ProvidersItem>? _providers;
private readonly int _delayTimeout = 99999999;
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
public BulkObservableCollection<ClashProxyModel> ProxyDetails { get; } = [];
[Reactive]
public partial ClashProxyModel SelectedGroup { get; set; }
[Reactive]
public partial ClashProxyModel SelectedDetail { get; set; }
public ReactiveCommand<RxVoid, RxVoid> ProxiesReloadCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> ProxiesDelayTestCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> ProxiesDelayTestPartCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> ProxiesSelectActivityCmd { get; }
[Reactive]
public partial int RuleModeSelected { get; set; }
[Reactive]
public partial int SortingSelected { get; set; }
[Reactive]
public partial bool AutoRefresh { get; set; }
private ClashItem _clashItem = new();
public ClashProxiesViewModel()
{
@@ -40,173 +13,158 @@ public partial class ClashProxiesViewModel : MyReactiveObject
{
await ProxiesReload();
});
ProxiesDelayTestCmd = ReactiveCommand.CreateFromTask(async () =>
ProxyDelayTestCmd = ReactiveCommand.CreateFromTask(async () =>
{
await ProxiesDelayTest(true);
if (!string.IsNullOrEmpty(SelectedDetail?.Name))
{
await TestProxyDelay(SelectedDetail.Name);
}
});
ProxiesDelayTestPartCmd = ReactiveCommand.CreateFromTask(async () =>
GroupProxiesDelayTestCmd = ReactiveCommand.CreateFromTask(async () =>
{
await ProxiesDelayTest(false);
await TestGroupProxiesDelay();
});
ProxiesSelectActivityCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SetActiveProxy();
});
SelectedGroup = new();
SelectedDetail = new();
AutoRefresh = _config.ClashUIItem.ProxiesAutoRefresh;
SortingSelected = _config.ClashUIItem.ProxiesSorting;
RuleModeSelected = (int)_config.ClashUIItem.RuleMode;
RuleModeSelected = nameof(ERuleMode.Rule);
#region WhenAnyValue && ReactiveCommand
this.WhenAnyValue(
x => x.SelectedGroup,
y => y != null && y.Name.IsNotEmpty())
.Subscribe(RefreshProxyDetails);
this.WhenAnyValue(x => x.SelectedGroup)
.Where(y => y != null && y.Name.IsNotEmpty())
.Subscribe(_ => RefreshProxyDetails());
this.WhenAnyValue(
x => x.RuleModeSelected,
y => y >= 0)
.Subscribe(async c => await DoRuleModeSelected(c));
this.WhenAnyValue(x => x.RuleModeSelected)
.Where(y => !string.IsNullOrEmpty(y))
.Skip(1)
.SubscribeAsync(async x => await SetRuleMode(x));
this.WhenAnyValue(
x => x.SortingSelected,
y => y >= 0)
.Subscribe(DoSortingSelected);
this.WhenAnyValue(x => x.SortingSelected)
.Where(y => y >= 0)
.Subscribe(_ => DoSortingSelected());
this.WhenAnyValue(
x => x.AutoRefresh,
y => y == true)
.Subscribe(c => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; });
this.WhenAnyValue(x => x.AutoRefresh)
.Where(y => y)
.Subscribe(_ => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; });
#endregion WhenAnyValue && ReactiveCommand
_ = Init();
_ = Task.Factory.StartNew(
async () => await GetClashProxiesTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
}
private async Task Init()
{
await DelayTestTask();
}
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
public BulkObservableCollection<ClashProxyModel> ProxyDetails { get; } = [];
private async Task DoRuleModeSelected(bool c)
{
if (!c)
{
return;
}
if (_config.ClashUIItem.RuleMode == (ERuleMode)RuleModeSelected)
{
return;
}
await SetRuleModeCheck((ERuleMode)RuleModeSelected);
}
public BulkObservableCollection<string> ClashModes { get; } = new(Enum.GetNames<ERuleMode>().ToList());
public async Task SetRuleModeCheck(ERuleMode mode)
{
if (_config.ClashUIItem.RuleMode == mode)
{
return;
}
await SetRuleMode(mode);
}
[Reactive] public partial ClashProxyModel? SelectedGroup { get; set; }
private void DoSortingSelected(bool c)
[Reactive] public partial ClashProxyModel? SelectedDetail { get; set; }
public ReactiveCommand<RxVoid, RxVoid> ProxiesReloadCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> ProxyDelayTestCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> GroupProxiesDelayTestCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> ProxiesSelectActivityCmd { get; }
[Reactive] public partial string RuleModeSelected { get; set; }
[Reactive] public partial int SortingSelected { get; set; }
[Reactive] public partial bool AutoRefresh { get; set; }
private void DoSortingSelected()
{
if (!c)
{
return;
}
if (SortingSelected != _config.ClashUIItem.ProxiesSorting)
{
_config.ClashUIItem.ProxiesSorting = SortingSelected;
}
RefreshProxyDetails(c);
RefreshProxyDetails();
}
public async Task ProxiesReload()
{
await GetClashProxies(true);
await ProxiesDelayTest();
await GetClashProxies();
await GetClashModes();
}
#region task
public async Task GetClashProxiesTask()
{
var numOfExecuted = 1;
while (true)
{
await Task.Delay(1000 * 60);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0)
{
continue;
}
await GetClashProxies();
}
}
#endregion task
#region proxy function
private async Task SetRuleMode(ERuleMode mode)
private async Task SetRuleMode(string mode)
{
_config.ClashUIItem.RuleMode = mode;
if (mode != ERuleMode.Unchanged)
{
Dictionary<string, string> headers = new()
{
{ "mode", mode.ToString().ToLower() }
};
await ClashApiManager.Instance.ClashConfigUpdate(headers);
}
await ClashApiManager.Instance.UpdateClashMode(mode);
}
private async Task GetClashProxies(bool refreshUI)
private async Task GetClashProxies()
{
var ret = await ClashApiManager.Instance.GetClashProxiesAsync();
if (ret?.Item1 == null || ret.Item2 == null)
var ret = await ClashApiManager.Instance.GetProxies();
if (ret?.IsEmpty() != false)
{
return;
}
_proxies = ret.Item1.proxies;
_providers = ret?.Item2.providers;
_clashItem = ret;
if (refreshUI)
{
RxSchedulers.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups());
}
RxSchedulers.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups());
}
public async Task RefreshProxyGroups()
{
if (_proxies == null)
if (_clashItem.IsEmpty())
{
return;
}
var selectedName = SelectedGroup?.Name;
ProxyGroups.Clear();
var lstProxyGroups = new List<ClashProxyModel>();
var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups();
if (proxyGroups is { Count: > 0 })
{
foreach (var it in proxyGroups)
{
if (it.name.IsNullOrEmpty() || !_proxies.TryGetValue(it.name, out var item))
{
continue;
}
if (!Global.allowSelectType.Contains(item.type.ToLower()))
{
continue;
}
lstProxyGroups.Add(new ClashProxyModel()
{
Now = item.now,
Name = item.name,
Type = item.type
});
}
}
//from api
foreach (var kv in _proxies)
var globalName = "GLOBAL";
foreach (var kv in _clashItem.Proxies)
{
if (!Global.allowSelectType.Contains(kv.Value.type?.ToLower()))
{
continue;
}
if (kv.Key == "GLOBAL")
if (kv.Key == globalName)
{
continue;
}
@@ -215,61 +173,49 @@ public partial class ClashProxiesViewModel : MyReactiveObject
{
continue;
}
lstProxyGroups.Add(new ClashProxyModel()
lstProxyGroups.Add(new ClashProxyModel
{
Now = kv.Value.now,
Name = kv.Key,
Type = kv.Value.type,
});
}
if (_proxies.TryGetValue("GLOBAL", out var globalProxy))
if (_clashItem.Proxies.TryGetValue(globalName, out var globalProxy))
{
lstProxyGroups.Add(new ClashProxyModel()
lstProxyGroups.Add(new ClashProxyModel
{
Now = globalProxy.now,
Name = "GLOBAL",
Name = globalName,
Type = globalProxy.type,
});
}
ProxyGroups.AddRange(lstProxyGroups);
ProxyGroups.ReplaceRange(lstProxyGroups);
if (ProxyGroups is { Count: > 0 })
{
if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName))
{
SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName);
}
else
{
SelectedGroup = ProxyGroups.First();
}
SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName) ?? ProxyGroups.First();
}
else
{
SelectedGroup = new();
SelectedGroup = null;
}
await Task.CompletedTask;
}
private void RefreshProxyDetails(bool c)
private void RefreshProxyDetails()
{
ProxyDetails.Clear();
if (!c)
{
return;
}
var name = SelectedGroup?.Name;
if (name.IsNullOrEmpty())
{
return;
}
if (_proxies == null)
if (_clashItem.IsEmpty())
{
return;
}
_proxies.TryGetValue(name, out var proxy);
_clashItem.Proxies.TryGetValue(name, out var proxy);
if (proxy?.all == null)
{
return;
@@ -284,7 +230,7 @@ public partial class ClashProxiesViewModel : MyReactiveObject
}
var delay = proxy2.history?.Count > 0 ? proxy2.history.Last().delay : -1;
lstDetails.Add(new ClashProxyModel()
lstDetails.Add(new ClashProxyModel
{
IsActive = item == proxy.now,
Name = item,
@@ -293,7 +239,7 @@ public partial class ClashProxiesViewModel : MyReactiveObject
DelayName = delay <= 0 ? string.Empty : $"{delay}ms",
});
}
//sort
// sort
switch (SortingSelected)
{
case 0:
@@ -303,101 +249,95 @@ public partial class ClashProxiesViewModel : MyReactiveObject
case 1:
lstDetails = lstDetails.OrderBy(t => t.Name).ToList();
break;
default:
break;
}
ProxyDetails.AddRange(lstDetails);
ProxyDetails.ReplaceRange(lstDetails);
}
private ProxiesItem? TryGetProxy(string name)
private ClashProxy? TryGetProxy(string? name)
{
if (_proxies == null)
if (name.IsNullOrEmpty())
{
return null;
}
_proxies.TryGetValue(name, out var proxy2);
if (proxy2 != null)
{
return proxy2;
}
//from providers
if (_providers != null)
{
foreach (var kv in _providers)
{
if (Global.proxyVehicleType.Contains(kv.Value.vehicleType.ToLower()))
{
var proxy3 = kv.Value.proxies.FirstOrDefault(t => t.name == name);
if (proxy3 != null)
{
return proxy3;
}
}
}
}
return null;
_clashItem.Proxies.TryGetValue(name, out var proxy2);
return proxy2;
}
public async Task SetActiveProxy()
{
if (SelectedGroup == null || SelectedGroup.Name.IsNullOrEmpty())
if (SelectedGroup.Name.IsNullOrEmpty())
{
return;
}
if (SelectedDetail == null || SelectedDetail.Name.IsNullOrEmpty())
if (SelectedDetail.Name.IsNullOrEmpty())
{
return;
}
var name = SelectedGroup.Name;
if (name.IsNullOrEmpty())
var groupName = SelectedGroup.Name;
if (groupName.IsNullOrEmpty())
{
return;
}
var nameNode = SelectedDetail.Name;
if (nameNode.IsNullOrEmpty())
var nodeName = SelectedDetail.Name;
if (nodeName.IsNullOrEmpty())
{
return;
}
var selectedProxy = TryGetProxy(name);
if (selectedProxy == null || selectedProxy.type != "Selector")
var selectedProxy = TryGetProxy(groupName);
if (selectedProxy is not { type: "Selector" })
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return;
}
await ClashApiManager.Instance.ClashSetActiveProxy(name, nameNode);
selectedProxy.now = nameNode;
var group = ProxyGroups.FirstOrDefault(it => it.Name == SelectedGroup.Name);
if (group != null)
{
group.Now = nameNode;
var group2 = JsonUtils.DeepCopy(group);
ProxyGroups.Replace(group, group2);
SelectedGroup = group2;
}
await ClashApiManager.Instance.SetActiveProxy(groupName, nodeName);
await GetClashProxies();
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
}
private async Task ProxiesDelayTest(bool blAll = true)
private async Task GetClashModes()
{
ClashApiManager.Instance.ClashProxiesDelayTest(blAll, ProxyDetails.ToList(), async (item, result) =>
var ret = await ClashApiManager.Instance.GetClashModes();
if (ret is not { Count: > 0 })
{
if (item == null || result.IsNullOrEmpty())
{
return;
}
return;
}
ClashModes.ReplaceRange(ret);
var currentMode = await ClashApiManager.Instance.GetClashMode();
if (currentMode.IsNullOrEmpty())
{
return;
}
RuleModeSelected = currentMode;
}
var model = new SpeedTestResult() { IndexId = item.Name, Delay = result };
RxSchedulers.MainThreadScheduler.Schedule(() =>
{
_ = ProxiesDelayTestResult(model);
});
await Task.CompletedTask;
private async Task TestProxyDelay(string name)
{
var result = await ClashApiManager.Instance.TestDelay(name, _clashItem);
var model = new SpeedTestResult
{
IndexId = name,
Delay = result.ToString(),
};
await ProxiesDelayTestResult(model);
}
private async Task TestGroupProxiesDelay()
{
var groupProxy = TryGetProxy(SelectedGroup.Name);
if (!Global.allowSelectType.Contains(groupProxy?.type))
{
return;
}
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 4,
};
await Parallel.ForEachAsync(groupProxy?.all ?? [], options, async (name, _) =>
{
await TestProxyDelay(name);
});
await Task.CompletedTask;
}
public async Task ProxiesDelayTestResult(SpeedTestResult result)
@@ -407,56 +347,10 @@ public partial class ClashProxiesViewModel : MyReactiveObject
{
return;
}
var dicResult = JsonUtils.Deserialize<Dictionary<string, object>>(result.Delay);
if (dicResult != null && dicResult.TryGetValue("delay", out var value))
{
detail.Delay = Convert.ToInt32(value.ToString());
detail.DelayName = $"{detail.Delay}ms";
}
else if (dicResult != null && dicResult.TryGetValue("message", out var value1))
{
detail.Delay = _delayTimeout;
detail.DelayName = $"{value1}";
}
else
{
detail.Delay = _delayTimeout;
detail.DelayName = string.Empty;
}
detail.Delay = Convert.ToInt32(result.Delay);
detail.DelayName = $"{detail.Delay}ms";
await Task.CompletedTask;
}
#endregion proxy function
#region task
public async Task DelayTestTask()
{
_ = Task.Run(async () =>
{
var numOfExecuted = 1;
while (true)
{
await Task.Delay(1000 * 60);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0)
{
continue;
}
await ProxiesDelayTest();
}
});
await Task.CompletedTask;
}
#endregion task
}

View File

@@ -32,9 +32,11 @@
x:Name="cmbRulemode"
Width="100"
Margin="{StaticResource MarginLr8}">
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeRule}" />
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeGlobal}" />
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeDirect}" />
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Text="{Binding}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock
@@ -61,10 +63,10 @@
</Button>
<Button
x:Name="menuProxiesDelaytest"
x:Name="menuGroupProxiesDelaytest"
Margin="{StaticResource MarginLr8}"
Classes="IconButton Success"
ToolTip.Tip="{x:Static resx:ResUI.menuProxiesDelaytest}">
ToolTip.Tip="{x:Static resx:ResUI.menuDelaytest}">
<Button.Content>
<PathIcon Data="{StaticResource SemiIconBolt}" />
</Button.Content>
@@ -111,7 +113,7 @@
<ListBox x:Name="lstProxyDetails" ItemsSource="{Binding ProxyDetails}">
<ItemsControl.ContextMenu>
<ContextMenu>
<MenuItem x:Name="menuProxiesDelaytestPart" Header="{x:Static resx:ResUI.menuProxiesDelaytestPart}" />
<MenuItem x:Name="menuProxyDelaytest" Header="{x:Static resx:ResUI.menuDelaytest}" />
<MenuItem
x:Name="menuProxiesSelectActivity"
Header="{x:Static resx:ResUI.menuProxiesSelectActivity}"

View File

@@ -17,12 +17,13 @@ public partial class ClashProxiesView : ReactiveUserControl<ClashProxiesViewMode
this.Bind(ViewModel, vm => vm.SelectedDetail, v => v.lstProxyDetails.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxiesReloadCmd, v => v.menuProxiesReload).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxiesDelayTestCmd, v => v.menuProxiesDelaytest).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GroupProxiesDelayTestCmd, v => v.menuGroupProxiesDelaytest).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxiesDelayTestPartCmd, v => v.menuProxiesDelaytestPart).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxyDelayTestCmd, v => v.menuProxyDelaytest).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxiesSelectActivityCmd, v => v.menuProxiesSelectActivity).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RuleModeSelected, v => v.cmbRulemode.SelectedIndex).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ClashModes, v => v.cmbRulemode.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RuleModeSelected, v => v.cmbRulemode.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SortingSelected, v => v.cmbSorting.SelectedIndex).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
});

View File

@@ -37,9 +37,11 @@
Width="100"
Margin="{StaticResource MarginLeftRight8}"
Style="{StaticResource DefComboBox}">
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeRule}" />
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeGlobal}" />
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeDirect}" />
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock
@@ -68,12 +70,12 @@
</Button>
<Button
x:Name="menuProxiesDelaytest"
x:Name="menuGroupProxiesDelaytest"
Width="24"
Height="24"
Margin="{StaticResource MarginLeftRight8}"
Style="{StaticResource MaterialDesignFloatingActionMiniLightButton}"
ToolTip="{x:Static resx:ResUI.menuProxiesDelaytest}">
ToolTip="{x:Static resx:ResUI.menuDelaytest}">
<materialDesign:PackIcon VerticalAlignment="Center" Kind="LightningBolt" />
</Button>
@@ -141,7 +143,7 @@
Style="{StaticResource MaterialDesignListView}">
<ListView.ContextMenu>
<ContextMenu Style="{StaticResource DefContextMenu}">
<MenuItem x:Name="menuProxiesDelaytestPart" Header="{x:Static resx:ResUI.menuProxiesDelaytestPart}" />
<MenuItem x:Name="menuProxyDelaytest" Header="{x:Static resx:ResUI.menuDelaytest}" />
<MenuItem
x:Name="menuProxiesSelectActivity"
Header="{x:Static resx:ResUI.menuProxiesSelectActivity}"

View File

@@ -19,12 +19,13 @@ public partial class ClashProxiesView
this.Bind(ViewModel, vm => vm.SelectedDetail, v => v.lstProxyDetails.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxiesReloadCmd, v => v.menuProxiesReload).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxiesDelayTestCmd, v => v.menuProxiesDelaytest).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GroupProxiesDelayTestCmd, v => v.menuGroupProxiesDelaytest).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxiesDelayTestPartCmd, v => v.menuProxiesDelaytestPart).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxyDelayTestCmd, v => v.menuProxyDelaytest).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ProxiesSelectActivityCmd, v => v.menuProxiesSelectActivity).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RuleModeSelected, v => v.cmbRulemode.SelectedIndex).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ClashModes, v => v.cmbRulemode.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RuleModeSelected, v => v.cmbRulemode.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SortingSelected, v => v.cmbSorting.SelectedIndex).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
});