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
@@ -2,7 +2,14 @@ namespace ServiceLib.Base;
public class BulkObservableCollection<T> : ObservableCollection<T> 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) protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{ {
@@ -63,4 +70,29 @@ public class BulkObservableCollection<T> : ObservableCollection<T>
return true; 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));
}
}
} }
-1
View File
@@ -5,5 +5,4 @@ public enum ERuleMode
Rule = 0, Rule = 0,
Global = 1, Global = 1,
Direct = 2, Direct = 2,
Unchanged = 3
} }
+1 -2
View File
@@ -8,6 +8,7 @@ global using System.Net.NetworkInformation;
global using System.Net.Sockets; global using System.Net.Sockets;
global using ReactiveUI.Primitives; global using ReactiveUI.Primitives;
global using ReactiveUI.Primitives.Concurrency; global using ReactiveUI.Primitives.Concurrency;
global using ReactiveUI.Primitives.Extensions;
global using ReactiveUI.Primitives.Disposables; global using ReactiveUI.Primitives.Disposables;
global using ReactiveUI.Primitives.Signals; global using ReactiveUI.Primitives.Signals;
global using System.Reflection; global using System.Reflection;
@@ -41,5 +42,3 @@ global using ServiceLib.Services;
global using ServiceLib.Services.CoreConfig; global using ServiceLib.Services.CoreConfig;
global using ServiceLib.Services.Statistics; global using ServiceLib.Services.Statistics;
global using SQLite; global using SQLite;
+117 -103
View File
@@ -1,113 +1,104 @@
using static ServiceLib.Models.Dto.ClashProxies;
namespace ServiceLib.Manager; namespace ServiceLib.Manager;
public sealed class ClashApiManager public sealed class ClashApiManager
{ {
private const string _tag = "ClashApiManager";
private static readonly Lazy<ClashApiManager> instance = new(() => new()); private static readonly Lazy<ClashApiManager> instance = new(() => new());
public static ClashApiManager Instance => instance.Value; public static ClashApiManager Instance => instance.Value;
private static readonly string _tag = "ClashApiHandler"; private static string ApiUrl => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort2}";
private Dictionary<string, ProxiesItem>? _proxies;
public Dictionary<string, object> ProfileContent { get; set; }
public async Task<Tuple<ClashProxies, ClashProviders>?> GetClashProxiesAsync() public async Task<ClashItem?> GetProxies()
{ {
for (var i = 0; i < 3; i++) for (var i = 0; i < 3; i++)
{ {
var url = $"{GetApiUrl()}/proxies"; var url = $"{ApiUrl}/proxies";
var result = await HttpClientHelper.Instance.TryGetAsync(url); var resultTask = HttpClientHelper.Instance.TryGetAsync(url);
var clashProxies = JsonUtils.Deserialize<ClashProxies>(result);
var url2 = $"{GetApiUrl()}/providers/proxies"; var url2 = $"{ApiUrl}/providers/proxies";
var result2 = await HttpClientHelper.Instance.TryGetAsync(url2); 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); var clashProviders = JsonUtils.Deserialize<ClashProviders>(result2);
if (clashProxies != null || clashProviders != null) if (clashProxies is null && clashProviders is null)
{ {
_proxies = clashProxies?.proxies; await Task.Delay(2000);
return new Tuple<ClashProxies, ClashProviders>(clashProxies, clashProviders); 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; 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) return await TestProviderProxyDelay(name, providerName);
{ }
if (_proxies == null) return await TestProxyDelay(name);
{
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, "");
});
} }
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 try
{ {
var fileContent = ProfileContent; var url = $"{ApiUrl}/proxies/{Utils.UrlEncode(groupName)}";
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 headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
headers.Add("name", nameNode); headers.Add("name", nodeName);
await HttpClientHelper.Instance.PutAsync(url, headers); await HttpClientHelper.Instance.PutAsync(url, headers);
} }
catch (Exception ex) 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); await HttpClientHelper.Instance.PatchAsync(urlBase, headers);
} }
public async Task ClashConfigReload(string filePath) public async Task<List<string>> GetClashModes()
{ {
await ClashConnectionClose(""); var jsonNode = await GetConfig();
try if ((jsonNode?["mode-list"] ?? jsonNode?["modes"]) is not JsonArray { Count: > 0 } modesArray)
{ {
var url = $"{GetApiUrl()}/configs?force=true"; return [];
var headers = new Dictionary<string, string>();
headers.Add("path", filePath);
await HttpClientHelper.Instance.PutAsync(url, headers);
} }
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 try
{ {
var url = $"{GetApiUrl()}/connections"; var url = $"{ApiUrl}/connections";
var result = await HttpClientHelper.Instance.TryGetAsync(url); var result = await HttpClientHelper.Instance.TryGetAsync(url);
var clashConnections = JsonUtils.Deserialize<ClashConnections>(result); var clashConnections = JsonUtils.Deserialize<ClashConnections>(result);
@@ -162,11 +181,11 @@ public sealed class ClashApiManager
return null; return null;
} }
public async Task ClashConnectionClose(string id) public async Task CloseConnection(string id)
{ {
try try
{ {
var url = $"{GetApiUrl()}/connections/{id}"; var url = $"{ApiUrl}/connections/{id}";
await HttpClientHelper.Instance.DeleteAsync(url); await HttpClientHelper.Instance.DeleteAsync(url);
} }
catch (Exception ex) catch (Exception ex)
@@ -174,9 +193,4 @@ public sealed class ClashApiManager
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
} }
private string GetApiUrl()
{
return $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort2}";
}
} }
@@ -209,7 +209,6 @@ public class HysteriaItem
[Serializable] [Serializable]
public class ClashUIItem public class ClashUIItem
{ {
public ERuleMode RuleMode { get; set; }
public bool EnableIPv6 { get; set; } public bool EnableIPv6 { get; set; }
public bool EnableMixinContent { get; set; } public bool EnableMixinContent { get; set; }
public int ProxiesSorting { get; set; } public int ProxiesSorting { get; set; }
+44
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; } = [];
}
@@ -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; }
}
}
@@ -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; }
}
}
@@ -3,9 +3,9 @@ namespace ServiceLib.Models.Dto;
[Serializable] [Serializable]
public partial class ClashProxyModel : ReactiveObject 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; } public string? Now { get; set; }
+9 -9
View File
@@ -987,6 +987,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Latency Test 的本地化字符串。
/// </summary>
public static string menuDelaytest {
get {
return ResourceManager.GetString("menuDelaytest", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 DNS Settings 的本地化字符串。 /// 查找类似 DNS Settings 的本地化字符串。
/// </summary> /// </summary>
@@ -1401,15 +1410,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Latency Test 的本地化字符串。
/// </summary>
public static string menuProxiesDelaytest {
get {
return ResourceManager.GetString("menuProxiesDelaytest", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Part Node Latency Test 的本地化字符串。 /// 查找类似 Part Node Latency Test 的本地化字符串。
/// </summary> /// </summary>
+1 -1
View File
@@ -1215,7 +1215,7 @@
<data name="menuModeRule" xml:space="preserve"> <data name="menuModeRule" xml:space="preserve">
<value>قانون</value> <value>قانون</value>
</data> </data>
<data name="menuProxiesDelaytest" xml:space="preserve"> <data name="menuDelaytest" xml:space="preserve">
<value>تست تأخیر</value> <value>تست تأخیر</value>
</data> </data>
<data name="menuProxiesDelaytestPart" xml:space="preserve"> <data name="menuProxiesDelaytestPart" xml:space="preserve">
+1 -1
View File
@@ -1212,7 +1212,7 @@
<data name="menuModeRule" xml:space="preserve"> <data name="menuModeRule" xml:space="preserve">
<value>Règle</value> <value>Règle</value>
</data> </data>
<data name="menuProxiesDelaytest" xml:space="preserve"> <data name="menuDelaytest" xml:space="preserve">
<value>Test de latence</value> <value>Test de latence</value>
</data> </data>
<data name="menuProxiesDelaytestPart" xml:space="preserve"> <data name="menuProxiesDelaytestPart" xml:space="preserve">
+1 -1
View File
@@ -1215,7 +1215,7 @@
<data name="menuModeRule" xml:space="preserve"> <data name="menuModeRule" xml:space="preserve">
<value>Szabály</value> <value>Szabály</value>
</data> </data>
<data name="menuProxiesDelaytest" xml:space="preserve"> <data name="menuDelaytest" xml:space="preserve">
<value>Késleltetés teszt</value> <value>Késleltetés teszt</value>
</data> </data>
<data name="menuProxiesDelaytestPart" xml:space="preserve"> <data name="menuProxiesDelaytestPart" xml:space="preserve">
+1 -1
View File
@@ -1215,7 +1215,7 @@
<data name="menuModeRule" xml:space="preserve"> <data name="menuModeRule" xml:space="preserve">
<value>Aturan</value> <value>Aturan</value>
</data> </data>
<data name="menuProxiesDelaytest" xml:space="preserve"> <data name="menuDelaytest" xml:space="preserve">
<value>Uji latensi</value> <value>Uji latensi</value>
</data> </data>
<data name="menuProxiesDelaytestPart" xml:space="preserve"> <data name="menuProxiesDelaytestPart" xml:space="preserve">
+1 -1
View File
@@ -1221,7 +1221,7 @@
<data name="menuModeRule" xml:space="preserve"> <data name="menuModeRule" xml:space="preserve">
<value>Rule</value> <value>Rule</value>
</data> </data>
<data name="menuProxiesDelaytest" xml:space="preserve"> <data name="menuDelaytest" xml:space="preserve">
<value>Latency Test</value> <value>Latency Test</value>
</data> </data>
<data name="menuProxiesDelaytestPart" xml:space="preserve"> <data name="menuProxiesDelaytestPart" xml:space="preserve">
+1 -1
View File
@@ -1221,7 +1221,7 @@
<data name="menuModeRule" xml:space="preserve"> <data name="menuModeRule" xml:space="preserve">
<value>Правила</value> <value>Правила</value>
</data> </data>
<data name="menuProxiesDelaytest" xml:space="preserve"> <data name="menuDelaytest" xml:space="preserve">
<value>Тест задержки</value> <value>Тест задержки</value>
</data> </data>
<data name="menuProxiesDelaytestPart" xml:space="preserve"> <data name="menuProxiesDelaytestPart" xml:space="preserve">
+1 -1
View File
@@ -1218,7 +1218,7 @@
<data name="menuModeRule" xml:space="preserve"> <data name="menuModeRule" xml:space="preserve">
<value>规则</value> <value>规则</value>
</data> </data>
<data name="menuProxiesDelaytest" xml:space="preserve"> <data name="menuDelaytest" xml:space="preserve">
<value>延迟测试</value> <value>延迟测试</value>
</data> </data>
<data name="menuProxiesDelaytestPart" xml:space="preserve"> <data name="menuProxiesDelaytestPart" xml:space="preserve">
+1 -1
View File
@@ -1218,7 +1218,7 @@
<data name="menuModeRule" xml:space="preserve"> <data name="menuModeRule" xml:space="preserve">
<value>規則</value> <value>規則</value>
</data> </data>
<data name="menuProxiesDelaytest" xml:space="preserve"> <data name="menuDelaytest" xml:space="preserve">
<value>延遲測試</value> <value>延遲測試</value>
</data> </data>
<data name="menuProxiesDelaytestPart" xml:space="preserve"> <data name="menuProxiesDelaytestPart" xml:space="preserve">
@@ -94,17 +94,7 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
fileContent["ipv6"] = config.ClashUIItem.EnableIPv6; fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
//mode //mode
if (!fileContent.ContainsKey("mode")) fileContent.TryAdd("mode", nameof(ERuleMode.Rule));
{
fileContent["mode"] = nameof(ERuleMode.Rule).ToLower();
}
else
{
if (config.ClashUIItem.RuleMode != ERuleMode.Unchanged)
{
fileContent["mode"] = config.ClashUIItem.RuleMode.ToString().ToLower();
}
}
//enable tun mode //enable tun mode
if (isTunEnabled) if (isTunEnabled)
@@ -159,8 +149,6 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
return ret; return ret;
} }
ClashApiManager.Instance.ProfileContent = fileContent;
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}"); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}");
ret.Success = true; ret.Success = true;
return ret; return ret;
@@ -2,33 +2,18 @@ namespace ServiceLib.ViewModels;
public partial class ClashConnectionsViewModel : MyReactiveObject 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() public ClashConnectionsViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh; AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedSource, x => x.SelectedSource,
selectedSource => selectedSource != null && selectedSource.Id.IsNotEmpty()); selectedSource => selectedSource?.Id?.IsNotEmpty() == true);
this.WhenAnyValue( this.WhenAnyValue(
x => x.AutoRefresh, x => x.AutoRefresh)
y => y == true) .Subscribe(_ => { _config.ClashUIItem.ConnectionsAutoRefresh = AutoRefresh; });
.Subscribe(c => { _config.ClashUIItem.ConnectionsAutoRefresh = AutoRefresh; });
ConnectionCloseCmd = ReactiveCommand.CreateFromTask(async () => ConnectionCloseCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await ClashConnectionClose(false); await ClashConnectionClose(false);
@@ -39,17 +24,28 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await ClashConnectionClose(true); await ClashConnectionClose(true);
}); });
_ = Init(); _ = Task.Factory.StartNew(
async () => await GetClashConnectionsTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
} }
private async Task Init() public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
{
await DelayTestTask(); [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() private async Task GetClashConnections()
{ {
var ret = await ClashApiManager.Instance.GetClashConnectionsAsync(); var ret = await ClashApiManager.Instance.GetConnections();
if (ret == null) if (ret == null)
{ {
return; return;
@@ -69,7 +65,8 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
var lstModel = new List<ClashConnectionModel>(); var lstModel = new List<ClashConnectionModel>();
foreach (var item in connections ?? []) 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)) if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter))
{ {
continue; continue;
@@ -83,7 +80,7 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
Host = host, Host = host,
Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds, Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds,
Elapsed = (dtNow - item.start).ToString(@"hh\:mm\:ss"), 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); lstModel.Add(model);
@@ -103,47 +100,39 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
if (!all) if (!all)
{ {
var item = SelectedSource; var item = SelectedSource;
if (item is null) if (string.IsNullOrEmpty(item?.Id))
{ {
return; return;
} }
id = item.Id; id = item.Id;
} }
else await ClashApiManager.Instance.CloseConnection(id);
{
ConnectionItems.Clear();
}
await ClashApiManager.Instance.ClashConnectionClose(id);
await GetClashConnections(); await GetClashConnections();
} }
public async Task DelayTestTask() public async Task GetClashConnectionsTask()
{ {
_ = Task.Run(async () => var numOfExecuted = 1;
while (true)
{ {
var numOfExecuted = 1; await Task.Delay(1000 * 5);
while (true) numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{ {
await Task.Delay(1000 * 5); continue;
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();
} }
});
await Task.CompletedTask; if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{
continue;
}
await GetClashConnections();
}
} }
} }
@@ -1,36 +1,9 @@
using static ServiceLib.Models.Dto.ClashProviders;
using static ServiceLib.Models.Dto.ClashProxies;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public partial class ClashProxiesViewModel : MyReactiveObject public partial class ClashProxiesViewModel : MyReactiveObject
{ {
private Dictionary<string, ProxiesItem>? _proxies;
private Dictionary<string, ProvidersItem>? _providers;
private readonly int _delayTimeout = 99999999; private readonly int _delayTimeout = 99999999;
private ClashItem _clashItem = new();
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; }
public ClashProxiesViewModel() public ClashProxiesViewModel()
{ {
@@ -40,173 +13,158 @@ public partial class ClashProxiesViewModel : MyReactiveObject
{ {
await ProxiesReload(); 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 () => ProxiesSelectActivityCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SetActiveProxy(); await SetActiveProxy();
}); });
SelectedGroup = new();
SelectedDetail = new();
AutoRefresh = _config.ClashUIItem.ProxiesAutoRefresh; AutoRefresh = _config.ClashUIItem.ProxiesAutoRefresh;
SortingSelected = _config.ClashUIItem.ProxiesSorting; SortingSelected = _config.ClashUIItem.ProxiesSorting;
RuleModeSelected = (int)_config.ClashUIItem.RuleMode; RuleModeSelected = nameof(ERuleMode.Rule);
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
this.WhenAnyValue( this.WhenAnyValue(x => x.SelectedGroup)
x => x.SelectedGroup, .Where(y => y != null && y.Name.IsNotEmpty())
y => y != null && y.Name.IsNotEmpty()) .Subscribe(_ => RefreshProxyDetails());
.Subscribe(RefreshProxyDetails);
this.WhenAnyValue( this.WhenAnyValue(x => x.RuleModeSelected)
x => x.RuleModeSelected, .Where(y => !string.IsNullOrEmpty(y))
y => y >= 0) .Skip(1)
.Subscribe(async c => await DoRuleModeSelected(c)); .SubscribeAsync(async x => await SetRuleMode(x));
this.WhenAnyValue( this.WhenAnyValue(x => x.SortingSelected)
x => x.SortingSelected, .Where(y => y >= 0)
y => y >= 0) .Subscribe(_ => DoSortingSelected());
.Subscribe(DoSortingSelected);
this.WhenAnyValue( this.WhenAnyValue(x => x.AutoRefresh)
x => x.AutoRefresh, .Where(y => y)
y => y == true) .Subscribe(_ => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; });
.Subscribe(c => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; });
#endregion WhenAnyValue && ReactiveCommand #endregion WhenAnyValue && ReactiveCommand
_ = Init(); _ = Task.Factory.StartNew(
async () => await GetClashProxiesTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
} }
private async Task Init() public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
{ public BulkObservableCollection<ClashProxyModel> ProxyDetails { get; } = [];
await DelayTestTask();
}
private async Task DoRuleModeSelected(bool c) public BulkObservableCollection<string> ClashModes { get; } = new(Enum.GetNames<ERuleMode>().ToList());
{
if (!c)
{
return;
}
if (_config.ClashUIItem.RuleMode == (ERuleMode)RuleModeSelected)
{
return;
}
await SetRuleModeCheck((ERuleMode)RuleModeSelected);
}
public async Task SetRuleModeCheck(ERuleMode mode) [Reactive] public partial ClashProxyModel? SelectedGroup { get; set; }
{
if (_config.ClashUIItem.RuleMode == mode)
{
return;
}
await SetRuleMode(mode);
}
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) if (SortingSelected != _config.ClashUIItem.ProxiesSorting)
{ {
_config.ClashUIItem.ProxiesSorting = SortingSelected; _config.ClashUIItem.ProxiesSorting = SortingSelected;
} }
RefreshProxyDetails(c); RefreshProxyDetails();
} }
public async Task ProxiesReload() public async Task ProxiesReload()
{ {
await GetClashProxies(true); await GetClashProxies();
await ProxiesDelayTest(); 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 #region proxy function
private async Task SetRuleMode(ERuleMode mode) private async Task SetRuleMode(string mode)
{ {
_config.ClashUIItem.RuleMode = mode; await ClashApiManager.Instance.UpdateClashMode(mode);
if (mode != ERuleMode.Unchanged)
{
Dictionary<string, string> headers = new()
{
{ "mode", mode.ToString().ToLower() }
};
await ClashApiManager.Instance.ClashConfigUpdate(headers);
}
} }
private async Task GetClashProxies(bool refreshUI) private async Task GetClashProxies()
{ {
var ret = await ClashApiManager.Instance.GetClashProxiesAsync(); var ret = await ClashApiManager.Instance.GetProxies();
if (ret?.Item1 == null || ret.Item2 == null) if (ret?.IsEmpty() != false)
{ {
return; return;
} }
_proxies = ret.Item1.proxies; _clashItem = ret;
_providers = ret?.Item2.providers;
if (refreshUI) RxSchedulers.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups());
{
RxSchedulers.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups());
}
} }
public async Task RefreshProxyGroups() public async Task RefreshProxyGroups()
{ {
if (_proxies == null) if (_clashItem.IsEmpty())
{ {
return; return;
} }
var selectedName = SelectedGroup?.Name; var selectedName = SelectedGroup?.Name;
ProxyGroups.Clear();
var lstProxyGroups = new List<ClashProxyModel>(); 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 var globalName = "GLOBAL";
foreach (var kv in _proxies) foreach (var kv in _clashItem.Proxies)
{ {
if (!Global.allowSelectType.Contains(kv.Value.type?.ToLower())) if (!Global.allowSelectType.Contains(kv.Value.type?.ToLower()))
{ {
continue; continue;
} }
if (kv.Key == "GLOBAL") if (kv.Key == globalName)
{ {
continue; continue;
} }
@@ -215,61 +173,49 @@ public partial class ClashProxiesViewModel : MyReactiveObject
{ {
continue; continue;
} }
lstProxyGroups.Add(new ClashProxyModel() lstProxyGroups.Add(new ClashProxyModel
{ {
Now = kv.Value.now, Now = kv.Value.now,
Name = kv.Key, Name = kv.Key,
Type = kv.Value.type, 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, Now = globalProxy.now,
Name = "GLOBAL", Name = globalName,
Type = globalProxy.type, Type = globalProxy.type,
}); });
} }
ProxyGroups.AddRange(lstProxyGroups); ProxyGroups.ReplaceRange(lstProxyGroups);
if (ProxyGroups is { Count: > 0 }) if (ProxyGroups is { Count: > 0 })
{ {
if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName)) SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName) ?? ProxyGroups.First();
{
SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName);
}
else
{
SelectedGroup = ProxyGroups.First();
}
} }
else else
{ {
SelectedGroup = new(); SelectedGroup = null;
} }
await Task.CompletedTask; await Task.CompletedTask;
} }
private void RefreshProxyDetails(bool c) private void RefreshProxyDetails()
{ {
ProxyDetails.Clear();
if (!c)
{
return;
}
var name = SelectedGroup?.Name; var name = SelectedGroup?.Name;
if (name.IsNullOrEmpty()) if (name.IsNullOrEmpty())
{ {
return; return;
} }
if (_proxies == null) if (_clashItem.IsEmpty())
{ {
return; return;
} }
_proxies.TryGetValue(name, out var proxy); _clashItem.Proxies.TryGetValue(name, out var proxy);
if (proxy?.all == null) if (proxy?.all == null)
{ {
return; return;
@@ -284,7 +230,7 @@ public partial class ClashProxiesViewModel : MyReactiveObject
} }
var delay = proxy2.history?.Count > 0 ? proxy2.history.Last().delay : -1; var delay = proxy2.history?.Count > 0 ? proxy2.history.Last().delay : -1;
lstDetails.Add(new ClashProxyModel() lstDetails.Add(new ClashProxyModel
{ {
IsActive = item == proxy.now, IsActive = item == proxy.now,
Name = item, Name = item,
@@ -293,7 +239,7 @@ public partial class ClashProxiesViewModel : MyReactiveObject
DelayName = delay <= 0 ? string.Empty : $"{delay}ms", DelayName = delay <= 0 ? string.Empty : $"{delay}ms",
}); });
} }
//sort // sort
switch (SortingSelected) switch (SortingSelected)
{ {
case 0: case 0:
@@ -303,101 +249,95 @@ public partial class ClashProxiesViewModel : MyReactiveObject
case 1: case 1:
lstDetails = lstDetails.OrderBy(t => t.Name).ToList(); lstDetails = lstDetails.OrderBy(t => t.Name).ToList();
break; 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; return null;
} }
_proxies.TryGetValue(name, out var proxy2); _clashItem.Proxies.TryGetValue(name, out var proxy2);
if (proxy2 != null) return proxy2;
{
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;
} }
public async Task SetActiveProxy() public async Task SetActiveProxy()
{ {
if (SelectedGroup == null || SelectedGroup.Name.IsNullOrEmpty()) if (SelectedGroup.Name.IsNullOrEmpty())
{ {
return; return;
} }
if (SelectedDetail == null || SelectedDetail.Name.IsNullOrEmpty()) if (SelectedDetail.Name.IsNullOrEmpty())
{ {
return; return;
} }
var name = SelectedGroup.Name; var groupName = SelectedGroup.Name;
if (name.IsNullOrEmpty()) if (groupName.IsNullOrEmpty())
{ {
return; return;
} }
var nameNode = SelectedDetail.Name; var nodeName = SelectedDetail.Name;
if (nameNode.IsNullOrEmpty()) if (nodeName.IsNullOrEmpty())
{ {
return; return;
} }
var selectedProxy = TryGetProxy(name); var selectedProxy = TryGetProxy(groupName);
if (selectedProxy == null || selectedProxy.type != "Selector") if (selectedProxy is not { type: "Selector" })
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return; return;
} }
await ClashApiManager.Instance.ClashSetActiveProxy(name, nameNode); await ClashApiManager.Instance.SetActiveProxy(groupName, nodeName);
await GetClashProxies();
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;
}
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); 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 }; private async Task TestProxyDelay(string name)
RxSchedulers.MainThreadScheduler.Schedule(() => {
{ var result = await ClashApiManager.Instance.TestDelay(name, _clashItem);
_ = ProxiesDelayTestResult(model); var model = new SpeedTestResult
}); {
await Task.CompletedTask; 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) public async Task ProxiesDelayTestResult(SpeedTestResult result)
@@ -407,56 +347,10 @@ public partial class ClashProxiesViewModel : MyReactiveObject
{ {
return; return;
} }
detail.Delay = Convert.ToInt32(result.Delay);
var dicResult = JsonUtils.Deserialize<Dictionary<string, object>>(result.Delay); detail.DelayName = $"{detail.Delay}ms";
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;
}
await Task.CompletedTask; await Task.CompletedTask;
} }
#endregion proxy function #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
} }
@@ -32,9 +32,11 @@
x:Name="cmbRulemode" x:Name="cmbRulemode"
Width="100" Width="100"
Margin="{StaticResource MarginLr8}"> Margin="{StaticResource MarginLr8}">
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeRule}" /> <ComboBox.ItemTemplate>
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeGlobal}" /> <DataTemplate x:DataType="x:String">
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeDirect}" /> <TextBlock Text="{Binding}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
<TextBlock <TextBlock
@@ -61,10 +63,10 @@
</Button> </Button>
<Button <Button
x:Name="menuProxiesDelaytest" x:Name="menuGroupProxiesDelaytest"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Classes="IconButton Success" Classes="IconButton Success"
ToolTip.Tip="{x:Static resx:ResUI.menuProxiesDelaytest}"> ToolTip.Tip="{x:Static resx:ResUI.menuDelaytest}">
<Button.Content> <Button.Content>
<PathIcon Data="{StaticResource SemiIconBolt}" /> <PathIcon Data="{StaticResource SemiIconBolt}" />
</Button.Content> </Button.Content>
@@ -111,7 +113,7 @@
<ListBox x:Name="lstProxyDetails" ItemsSource="{Binding ProxyDetails}"> <ListBox x:Name="lstProxyDetails" ItemsSource="{Binding ProxyDetails}">
<ItemsControl.ContextMenu> <ItemsControl.ContextMenu>
<ContextMenu> <ContextMenu>
<MenuItem x:Name="menuProxiesDelaytestPart" Header="{x:Static resx:ResUI.menuProxiesDelaytestPart}" /> <MenuItem x:Name="menuProxyDelaytest" Header="{x:Static resx:ResUI.menuDelaytest}" />
<MenuItem <MenuItem
x:Name="menuProxiesSelectActivity" x:Name="menuProxiesSelectActivity"
Header="{x:Static resx:ResUI.menuProxiesSelectActivity}" Header="{x:Static resx:ResUI.menuProxiesSelectActivity}"
@@ -17,12 +17,13 @@ public partial class ClashProxiesView : ReactiveUserControl<ClashProxiesViewMode
this.Bind(ViewModel, vm => vm.SelectedDetail, v => v.lstProxyDetails.SelectedItem).DisposeWith(disposables); 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.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.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.SortingSelected, v => v.cmbSorting.SelectedIndex).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
}); });
+8 -6
View File
@@ -37,9 +37,11 @@
Width="100" Width="100"
Margin="{StaticResource MarginLeftRight8}" Margin="{StaticResource MarginLeftRight8}"
Style="{StaticResource DefComboBox}"> Style="{StaticResource DefComboBox}">
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeRule}" /> <ComboBox.ItemTemplate>
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeGlobal}" /> <DataTemplate>
<ComboBoxItem Content="{x:Static resx:ResUI.menuModeDirect}" /> <TextBlock Text="{Binding}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
<TextBlock <TextBlock
@@ -68,12 +70,12 @@
</Button> </Button>
<Button <Button
x:Name="menuProxiesDelaytest" x:Name="menuGroupProxiesDelaytest"
Width="24" Width="24"
Height="24" Height="24"
Margin="{StaticResource MarginLeftRight8}" Margin="{StaticResource MarginLeftRight8}"
Style="{StaticResource MaterialDesignFloatingActionMiniLightButton}" Style="{StaticResource MaterialDesignFloatingActionMiniLightButton}"
ToolTip="{x:Static resx:ResUI.menuProxiesDelaytest}"> ToolTip="{x:Static resx:ResUI.menuDelaytest}">
<materialDesign:PackIcon VerticalAlignment="Center" Kind="LightningBolt" /> <materialDesign:PackIcon VerticalAlignment="Center" Kind="LightningBolt" />
</Button> </Button>
@@ -141,7 +143,7 @@
Style="{StaticResource MaterialDesignListView}"> Style="{StaticResource MaterialDesignListView}">
<ListView.ContextMenu> <ListView.ContextMenu>
<ContextMenu Style="{StaticResource DefContextMenu}"> <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 <MenuItem
x:Name="menuProxiesSelectActivity" x:Name="menuProxiesSelectActivity"
Header="{x:Static resx:ResUI.menuProxiesSelectActivity}" Header="{x:Static resx:ResUI.menuProxiesSelectActivity}"
+4 -3
View File
@@ -19,12 +19,13 @@ public partial class ClashProxiesView
this.Bind(ViewModel, vm => vm.SelectedDetail, v => v.lstProxyDetails.SelectedItem).DisposeWith(disposables); 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.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.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.SortingSelected, v => v.cmbSorting.SelectedIndex).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
}); });