mirror of
https://github.com/2dust/v2rayN.git
synced 2026-09-26 00:22:07 +03:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77c462ec7b | |||
| 526cd6834d | |||
| 7be23d7ebc | |||
| d952c180c9 | |||
| 80fa6575a3 | |||
| f05e44d7d9 | |||
| b10c16a94a | |||
| 143ada1358 | |||
| d8c7bd9d02 | |||
| 85666af36a | |||
| 7286c67a78 | |||
| 8b8e579bc8 | |||
| ed4dd7f6d9 | |||
| bb62c9eda5 | |||
| 3d50d9f82c |
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>7.25.0</Version>
|
<Version>7.25.1</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
||||||
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
|
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
|
||||||
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
||||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
|
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.2" />
|
||||||
<PackageVersion Include="CliWrap" Version="3.10.5" />
|
<PackageVersion Include="CliWrap" Version="3.10.5" />
|
||||||
<PackageVersion Include="Downloader" Version="5.9.6" />
|
<PackageVersion Include="Downloader" Version="5.9.6" />
|
||||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
||||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
||||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||||
<PackageVersion Include="TUnit" Version="1.65.68" />
|
<PackageVersion Include="TUnit" Version="1.66.27" />
|
||||||
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
|
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
|
||||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||||
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ public class CoreConfigV2rayServiceTests
|
|||||||
await headers["Set-Cookie"]!.AsArray()
|
await headers["Set-Cookie"]!.AsArray()
|
||||||
.Select(item => item!.GetValue<string>())
|
.Select(item => item!.GetValue<string>())
|
||||||
.Should().BeEquivalentTo(["a=1", "b=2"]);
|
.Should().BeEquivalentTo(["a=1", "b=2"]);
|
||||||
await outbound.settings.servers.Should().BeNull();
|
|
||||||
await outbound.settings.vnext.Should().BeNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -714,6 +712,5 @@ public class CoreConfigV2rayServiceTests
|
|||||||
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
|
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
|
||||||
await proxyOutbound.Should().NotBeNull();
|
await proxyOutbound.Should().NotBeNull();
|
||||||
await proxyOutbound!.protocol.Should().BeEqualTo("shadowsocks");
|
await proxyOutbound!.protocol.Should().BeEqualTo("shadowsocks");
|
||||||
await proxyOutbound.settings.servers.Should().NotBeNull();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
namespace ServiceLib.Tests.Helper;
|
||||||
|
|
||||||
|
public class HttpRequestHeadersHelperTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task TryParse_ShouldAcceptEmptySettingsForExistingSubscriptions()
|
||||||
|
{
|
||||||
|
foreach (var json in new string?[] { null, "", " \r\n ", "{}" })
|
||||||
|
{
|
||||||
|
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
|
||||||
|
await headers.Count.Should().BeEqualTo(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TryParse_ShouldPreserveValuesAndUseCaseInsensitiveNames()
|
||||||
|
{
|
||||||
|
const string json = """
|
||||||
|
{
|
||||||
|
"X-hwid": "my_test_device",
|
||||||
|
"Authorization": "Bearer test:token",
|
||||||
|
"accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Empty": ""
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
|
||||||
|
await headers["x-HWID"].Should().BeEqualTo("my_test_device");
|
||||||
|
await headers["AUTHORIZATION"].Should().BeEqualTo("Bearer test:token");
|
||||||
|
await headers["Accept"].Should().BeEqualTo("application/json");
|
||||||
|
await headers["Content-Type"].Should().BeEqualTo("application/json");
|
||||||
|
await headers["X-Empty"].Should().BeEqualTo("");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("not-json")]
|
||||||
|
[Arguments("null")]
|
||||||
|
[Arguments("[]")]
|
||||||
|
[Arguments("{\"X-Test\": 1}")]
|
||||||
|
[Arguments("{\"X-Test\": null}")]
|
||||||
|
[Arguments("{\"X-Test\": [\"one\", \"two\"]}")]
|
||||||
|
[Arguments("{\"X-Test\": \"one\", \"X-Test\": \"two\"}")]
|
||||||
|
[Arguments("{\"Accept\": \"one\", \"accept\": \"two\"}")]
|
||||||
|
[Arguments("{\"Bad Header\": \"value\"}")]
|
||||||
|
[Arguments("{\"Bad:Header\": \"value\"}")]
|
||||||
|
[Arguments("{\"\": \"value\"}")]
|
||||||
|
[Arguments("{\"X-Test\": \"one\\r\\nInjected: two\"}")]
|
||||||
|
[Arguments("{\"X-Test\": \"one\\nInjected: two\"}")]
|
||||||
|
[Arguments("{\"X-Test\": \"one\\u0000two\"}")]
|
||||||
|
public async Task TryParse_ShouldRejectInvalidHeadersWithoutReturningPartialSettings(string json)
|
||||||
|
{
|
||||||
|
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeFalse();
|
||||||
|
await headers.Count.Should().BeEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task RequestHeaders_ShouldSurviveDatabaseMigrationAndEditing()
|
||||||
|
{
|
||||||
|
using var database = new SQLiteConnection(":memory:", false);
|
||||||
|
database.Execute("CREATE TABLE SubItem (Id TEXT PRIMARY KEY, Remarks TEXT, Url TEXT)");
|
||||||
|
database.Execute("INSERT INTO SubItem (Id, Remarks, Url) VALUES (?, ?, ?)", "existing", "Existing", "https://example.com/sub");
|
||||||
|
database.CreateTable<SubItem>();
|
||||||
|
|
||||||
|
var item = database.Find<SubItem>("existing");
|
||||||
|
await HttpRequestHeadersHelper.TryParse(item.RequestHeaders, out var oldHeaders).Should().BeTrue();
|
||||||
|
await oldHeaders.Count.Should().BeEqualTo(0);
|
||||||
|
|
||||||
|
item.RequestHeaders = "{\"X-hwid\":\"my_device\"}";
|
||||||
|
database.Update(item);
|
||||||
|
await database.Find<SubItem>(item.Id).RequestHeaders.Should().BeEqualTo(item.RequestHeaders);
|
||||||
|
|
||||||
|
item.RequestHeaders = "";
|
||||||
|
database.Update(item);
|
||||||
|
await database.Find<SubItem>(item.Id).RequestHeaders.Should().BeEqualTo("");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
namespace ServiceLib.Tests.Services;
|
||||||
|
|
||||||
|
public class DownloadServiceHeadersTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
[Arguments(false, false)]
|
||||||
|
[Arguments(false, true)]
|
||||||
|
[Arguments(true, false)]
|
||||||
|
[Arguments(true, true)]
|
||||||
|
public async Task TryDownloadString_ShouldSendCustomHeadersThroughBothDownloaders(bool useProxy, bool failFirstRequest)
|
||||||
|
{
|
||||||
|
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
|
||||||
|
await using var server = new SubscriptionHttpServer(failFirstRequest);
|
||||||
|
const string json = """
|
||||||
|
{
|
||||||
|
"accept": "application/json",
|
||||||
|
"user-agent": "CustomSubscriptionClient/1.0",
|
||||||
|
"authorization": "Bearer test-token",
|
||||||
|
"X-hwid": "test-device",
|
||||||
|
"Cookie": "session=test",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
|
||||||
|
var service = new DownloadService { AcceptHeader = "*/*", RequestHeaders = headers };
|
||||||
|
var uri = new UriBuilder(useProxy ? "http://subscription.invalid/sub" : server.Url)
|
||||||
|
{
|
||||||
|
UserName = "user",
|
||||||
|
Password = "password"
|
||||||
|
}.Uri;
|
||||||
|
IWebProxy? proxy = useProxy ? new WebProxy(server.Url) : null;
|
||||||
|
|
||||||
|
var content = await service.TryDownloadString(uri.AbsoluteUri, proxy, "OriginalClient/1.0").WaitAsync(TimeSpan.FromSeconds(20));
|
||||||
|
|
||||||
|
await content.Should().BeEqualTo(SubscriptionHttpServer.Body);
|
||||||
|
await (server.Requests.Count >= (failFirstRequest ? 2 : 1)).Should().BeTrue();
|
||||||
|
foreach (var request in server.Requests)
|
||||||
|
{
|
||||||
|
await request["Accept"].Should().BeEqualTo("application/json");
|
||||||
|
await request["User-Agent"].Should().BeEqualTo("CustomSubscriptionClient/1.0");
|
||||||
|
await request["Authorization"].Should().BeEqualTo("Bearer test-token");
|
||||||
|
await request["X-hwid"].Should().BeEqualTo("test-device");
|
||||||
|
await request["Cookie"].Should().BeEqualTo("session=test");
|
||||||
|
await request["Content-Type"].Should().BeEqualTo("application/json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(false)]
|
||||||
|
[Arguments(true)]
|
||||||
|
public async Task TryDownloadString_ShouldKeepDefaultAcceptUserAgentAndBasicAuth(bool failFirstRequest)
|
||||||
|
{
|
||||||
|
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
|
||||||
|
await using var server = new SubscriptionHttpServer(failFirstRequest);
|
||||||
|
var service = new DownloadService { AcceptHeader = "*/*" };
|
||||||
|
var uri = new UriBuilder(server.Url) { UserName = "user", Password = "password" }.Uri;
|
||||||
|
|
||||||
|
var content = await service.TryDownloadString(uri.AbsoluteUri, (IWebProxy?)null, "ExistingClient/1.0").WaitAsync(TimeSpan.FromSeconds(20));
|
||||||
|
|
||||||
|
await content.Should().BeEqualTo(SubscriptionHttpServer.Body);
|
||||||
|
foreach (var request in server.Requests)
|
||||||
|
{
|
||||||
|
await request["Accept"].Should().BeEqualTo("*/*");
|
||||||
|
await request["User-Agent"].Should().BeEqualTo("ExistingClient/1.0");
|
||||||
|
await request["Authorization"].Should().BeEqualTo("Basic dXNlcjpwYXNzd29yZA==");
|
||||||
|
await request.ContainsKey("X-hwid").Should().BeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TryDownloadString_ShouldNotShareHeadersWithOtherDownloads()
|
||||||
|
{
|
||||||
|
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
|
||||||
|
await using var server = new SubscriptionHttpServer();
|
||||||
|
var subscription = new DownloadService
|
||||||
|
{
|
||||||
|
AcceptHeader = "*/*",
|
||||||
|
RequestHeaders = new Dictionary<string, string> { ["X-hwid"] = "first-device" }
|
||||||
|
};
|
||||||
|
var ordinaryDownload = new DownloadService();
|
||||||
|
|
||||||
|
await (await subscription.TryDownloadString(server.Url, (IWebProxy?)null, "TestClient/1.0")).Should().BeEqualTo(SubscriptionHttpServer.Body);
|
||||||
|
await (await ordinaryDownload.TryDownloadString(server.Url, (IWebProxy?)null, "TestClient/1.0")).Should().BeEqualTo(SubscriptionHttpServer.Body);
|
||||||
|
|
||||||
|
var requests = server.Requests.ToArray();
|
||||||
|
await requests.Length.Should().BeEqualTo(2);
|
||||||
|
await requests[0]["X-hwid"].Should().BeEqualTo("first-device");
|
||||||
|
await requests[1].ContainsKey("X-hwid").Should().BeFalse();
|
||||||
|
await requests[1].ContainsKey("Accept").Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SubscriptionHttpServer : IAsyncDisposable
|
||||||
|
{
|
||||||
|
public const string Body = "subscription-test-content";
|
||||||
|
private readonly TcpListener _listener = new(IPAddress.Loopback, 0);
|
||||||
|
private readonly CancellationTokenSource _cancellation = new();
|
||||||
|
private readonly Task _serverTask;
|
||||||
|
private readonly bool _failFirstRequest;
|
||||||
|
|
||||||
|
public string Url { get; }
|
||||||
|
public ConcurrentQueue<Dictionary<string, string>> Requests { get; } = new();
|
||||||
|
|
||||||
|
public SubscriptionHttpServer(bool failFirstRequest = false)
|
||||||
|
{
|
||||||
|
_failFirstRequest = failFirstRequest;
|
||||||
|
_listener.Start();
|
||||||
|
Url = $"http://127.0.0.1:{((IPEndPoint)_listener.LocalEndpoint).Port}/subscription";
|
||||||
|
_serverTask = ServeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ServeAsync()
|
||||||
|
{
|
||||||
|
var cancellationToken = _cancellation.Token;
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
using var client = await _listener.AcceptTcpClientAsync(cancellationToken);
|
||||||
|
await using var stream = client.GetStream();
|
||||||
|
using var reader = new StreamReader(stream, Encoding.ASCII, leaveOpen: true);
|
||||||
|
var requestLine = await reader.ReadLineAsync(cancellationToken);
|
||||||
|
if (requestLine == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
while (await reader.ReadLineAsync(cancellationToken) is { Length: > 0 } line)
|
||||||
|
{
|
||||||
|
var separator = line.IndexOf(':');
|
||||||
|
var name = line.Substring(0, separator);
|
||||||
|
var value = line.Substring(separator + 1).Trim();
|
||||||
|
headers[name] = headers.TryGetValue(name, out var previous) ? $"{previous}, {value}" : value;
|
||||||
|
}
|
||||||
|
Requests.Enqueue(headers);
|
||||||
|
|
||||||
|
var status = _failFirstRequest && Requests.Count == 1 ? "503 Service Unavailable" : "200 OK";
|
||||||
|
var body = requestLine.StartsWith("HEAD ") ? "" : Body;
|
||||||
|
var response = $"HTTP/1.1 {status}\r\nContent-Length: {Body.Length}\r\nConnection: close\r\n\r\n{body}";
|
||||||
|
await stream.WriteAsync(Encoding.ASCII.GetBytes(response), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await _cancellation.CancelAsync();
|
||||||
|
_listener.Stop();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _serverTask;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_cancellation.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -489,16 +489,22 @@ public class Global
|
|||||||
"localhost"
|
"localhost"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static readonly List<LanguageOption> LanguageOptions =
|
||||||
|
[
|
||||||
|
new("zh-Hans", "简体中文"),
|
||||||
|
new("zh-Hant", "繁體中文"),
|
||||||
|
new("en", "English"),
|
||||||
|
new("fa", "فارسی"),
|
||||||
|
new("fr", "Français"),
|
||||||
|
new("ru", "Русский"),
|
||||||
|
new("hu", "Magyar"),
|
||||||
|
new("id", "Bahasa Indonesia"),
|
||||||
|
new("az", "Azərbaycan dili")
|
||||||
|
];
|
||||||
|
|
||||||
public static readonly List<string> Languages =
|
public static readonly List<string> Languages =
|
||||||
[
|
[
|
||||||
"zh-Hans",
|
.. LanguageOptions.Select(t => t.Value)
|
||||||
"zh-Hant",
|
|
||||||
"en",
|
|
||||||
"fa",
|
|
||||||
"fr",
|
|
||||||
"ru",
|
|
||||||
"hu",
|
|
||||||
"id"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly List<string> Alpns =
|
public static readonly List<string> Alpns =
|
||||||
|
|||||||
@@ -471,12 +471,12 @@ public static class ConfigHandler
|
|||||||
/// Supports moving to top, up, down, bottom or specific position
|
/// Supports moving to top, up, down, bottom or specific position
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="config">Current configuration</param>
|
/// <param name="config">Current configuration</param>
|
||||||
/// <param name="lstProfile">List of server profiles</param>
|
/// <param name="lstProfile">List of server profile index ids</param>
|
||||||
/// <param name="index">Index of the server to move</param>
|
/// <param name="index">Index of the server to move</param>
|
||||||
/// <param name="eMove">Direction to move the server</param>
|
/// <param name="eMove">Direction to move the server</param>
|
||||||
/// <param name="pos">Target position when using EMove.Position</param>
|
/// <param name="pos">Target position when using EMove.Position</param>
|
||||||
/// <returns>0 if successful, -1 if failed</returns>
|
/// <returns>0 if successful, -1 if failed</returns>
|
||||||
public static async Task<int> MoveServer(Config config, List<ProfileItem> lstProfile, int index, EMove eMove, int pos = -1)
|
public static async Task<int> MoveServer(Config config, List<string> lstProfile, int index, EMove eMove, int pos = -1)
|
||||||
{
|
{
|
||||||
var count = lstProfile.Count;
|
var count = lstProfile.Count;
|
||||||
if (index < 0 || index > lstProfile.Count - 1)
|
if (index < 0 || index > lstProfile.Count - 1)
|
||||||
@@ -486,7 +486,7 @@ public static class ConfigHandler
|
|||||||
|
|
||||||
for (var i = 0; i < lstProfile.Count; i++)
|
for (var i = 0; i < lstProfile.Count; i++)
|
||||||
{
|
{
|
||||||
ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10);
|
ProfileExManager.Instance.SetSort(lstProfile[i], (i + 1) * 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sort = 0;
|
var sort = 0;
|
||||||
@@ -498,7 +498,7 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
sort = ProfileExManager.Instance.GetSort(lstProfile.First().IndexId) - 1;
|
sort = ProfileExManager.Instance.GetSort(lstProfile.First()) - 1;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -508,7 +508,7 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
sort = ProfileExManager.Instance.GetSort(lstProfile[index - 1].IndexId) - 1;
|
sort = ProfileExManager.Instance.GetSort(lstProfile[index - 1]) - 1;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -519,7 +519,7 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
sort = ProfileExManager.Instance.GetSort(lstProfile[index + 1].IndexId) + 1;
|
sort = ProfileExManager.Instance.GetSort(lstProfile[index + 1]) + 1;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -529,7 +529,7 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
sort = ProfileExManager.Instance.GetSort(lstProfile[^1].IndexId) + 1;
|
sort = ProfileExManager.Instance.GetSort(lstProfile[^1]) + 1;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -538,7 +538,7 @@ public static class ConfigHandler
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfileExManager.Instance.SetSort(lstProfile[index].IndexId, sort);
|
ProfileExManager.Instance.SetSort(lstProfile[index], sort);
|
||||||
return await Task.FromResult(0);
|
return await Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2202,6 +2202,7 @@ public static class ConfigHandler
|
|||||||
item.Enabled = subItem.Enabled;
|
item.Enabled = subItem.Enabled;
|
||||||
item.AutoUpdateInterval = subItem.AutoUpdateInterval;
|
item.AutoUpdateInterval = subItem.AutoUpdateInterval;
|
||||||
item.UserAgent = subItem.UserAgent;
|
item.UserAgent = subItem.UserAgent;
|
||||||
|
item.RequestHeaders = subItem.RequestHeaders;
|
||||||
item.Sort = subItem.Sort;
|
item.Sort = subItem.Sort;
|
||||||
item.Filter = subItem.Filter;
|
item.Filter = subItem.Filter;
|
||||||
item.UpdateTime = subItem.UpdateTime;
|
item.UpdateTime = subItem.UpdateTime;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public static class SubscriptionHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create download handler
|
// Create download handler
|
||||||
var downloadHandle = CreateDownloadHandler(hashCode, updateFunc);
|
var downloadHandle = CreateDownloadHandler(item, hashCode, updateFunc);
|
||||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
|
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
|
||||||
|
|
||||||
// Get all subscription content (main subscription + additional subscriptions)
|
// Get all subscription content (main subscription + additional subscriptions)
|
||||||
@@ -80,9 +80,18 @@ public static class SubscriptionHandler
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DownloadService CreateDownloadHandler(string hashCode, Func<bool, string, Task> updateFunc)
|
private static DownloadService CreateDownloadHandler(SubItem item, string hashCode, Func<bool, string, Task> updateFunc)
|
||||||
{
|
{
|
||||||
var downloadHandle = new DownloadService { AcceptHeader = "*/*" };
|
if (!HttpRequestHeadersHelper.TryParse(item.RequestHeaders, out var requestHeaders))
|
||||||
|
{
|
||||||
|
throw new FormatException(ResUI.SubRequestHeadersInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadHandle = new DownloadService
|
||||||
|
{
|
||||||
|
AcceptHeader = "*/*",
|
||||||
|
RequestHeaders = requestHeaders
|
||||||
|
};
|
||||||
downloadHandle.Error += (sender2, args) =>
|
downloadHandle.Error += (sender2, args) =>
|
||||||
{
|
{
|
||||||
updateFunc?.Invoke(false, $"{hashCode}{args.GetException().Message}");
|
updateFunc?.Invoke(false, $"{hashCode}{args.GetException().Message}");
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ public class DownloaderHelper
|
|||||||
private static readonly Lazy<DownloaderHelper> _instance = new(() => new());
|
private static readonly Lazy<DownloaderHelper> _instance = new(() => new());
|
||||||
public static DownloaderHelper Instance => _instance.Value;
|
public static DownloaderHelper Instance => _instance.Value;
|
||||||
|
|
||||||
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout)
|
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout,
|
||||||
|
IReadOnlyDictionary<string, string>? requestHeaders = null, string? acceptHeader = null)
|
||||||
{
|
{
|
||||||
if (url.IsNullOrEmpty())
|
if (url.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -28,6 +29,7 @@ public class DownloaderHelper
|
|||||||
var requestConfiguration = new RequestConfiguration()
|
var requestConfiguration = new RequestConfiguration()
|
||||||
{
|
{
|
||||||
Headers = headers,
|
Headers = headers,
|
||||||
|
Accept = acceptHeader,
|
||||||
UserAgent = userAgent,
|
UserAgent = userAgent,
|
||||||
ConnectTimeout = connectTimeout * 1000,
|
ConnectTimeout = connectTimeout * 1000,
|
||||||
Proxy = webProxy
|
Proxy = webProxy
|
||||||
@@ -37,7 +39,7 @@ public class DownloaderHelper
|
|||||||
BlockTimeout = timeout * 1000,
|
BlockTimeout = timeout * 1000,
|
||||||
MaxTryAgainOnFailure = 2,
|
MaxTryAgainOnFailure = 2,
|
||||||
RequestConfiguration = requestConfiguration,
|
RequestConfiguration = requestConfiguration,
|
||||||
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
CustomHttpMessageHandlerFactory = () => HttpRequestHeadersHelper.CreateHandler(GetSocketsHttpHandler(requestConfiguration), requestHeaders),
|
||||||
};
|
};
|
||||||
|
|
||||||
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
namespace ServiceLib.Helper;
|
||||||
|
|
||||||
|
public static class HttpRequestHeadersHelper
|
||||||
|
{
|
||||||
|
public static bool TryParse(string? json, out Dictionary<string, string> headers)
|
||||||
|
{
|
||||||
|
headers = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(json);
|
||||||
|
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
using var request = new HttpRequestMessage { Content = new ByteArrayContent([]) };
|
||||||
|
foreach (var property in document.RootElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (property.Value.ValueKind != JsonValueKind.String
|
||||||
|
|| !parsed.TryAdd(property.Name, property.Value.GetString()!)
|
||||||
|
|| !TryAddHeader(request, property.Name, parsed[property.Name]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is JsonException or ArgumentException or FormatException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpMessageHandler CreateHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary<string, string>? headers)
|
||||||
|
{
|
||||||
|
return headers is { Count: > 0 } ? new RequestHeadersHandler(innerHandler, headers) : innerHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryAddHeader(HttpRequestMessage request, string name, string value)
|
||||||
|
{
|
||||||
|
if (value.Any(c => char.IsControl(c) && c != '\t'))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.Headers.TryAddWithoutValidation(name, value)
|
||||||
|
|| request.Content!.Headers.TryAddWithoutValidation(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RequestHeadersHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary<string, string> headers)
|
||||||
|
: DelegatingHandler(innerHandler)
|
||||||
|
{
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var customHeaders = new HttpRequestMessage { Content = new ByteArrayContent([]) };
|
||||||
|
foreach (var header in headers)
|
||||||
|
{
|
||||||
|
if (!TryAddHeader(customHeaders, header.Key, header.Value))
|
||||||
|
{
|
||||||
|
throw new FormatException(ResUI.SubRequestHeadersInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply after each downloader's defaults, replacing headers without changing their values.
|
||||||
|
foreach (var header in customHeaders.Headers.NonValidated)
|
||||||
|
{
|
||||||
|
request.Headers.Remove(header.Key);
|
||||||
|
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
foreach (var header in customHeaders.Content.Headers.NonValidated)
|
||||||
|
{
|
||||||
|
request.Content ??= new ByteArrayContent([]);
|
||||||
|
request.Content.Headers.Remove(header.Key);
|
||||||
|
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.SendAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,8 +75,6 @@ public class Inboundsettings4Ray
|
|||||||
|
|
||||||
public string? address { get; set; }
|
public string? address { get; set; }
|
||||||
|
|
||||||
public List<UsersItem4Ray>? clients { get; set; }
|
|
||||||
|
|
||||||
public string? decryption { get; set; }
|
public string? decryption { get; set; }
|
||||||
|
|
||||||
public bool? allowTransparent { get; set; }
|
public bool? allowTransparent { get; set; }
|
||||||
@@ -96,21 +94,6 @@ public class Inboundsettings4Ray
|
|||||||
public List<string>? dns { get; set; }
|
public List<string>? dns { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UsersItem4Ray
|
|
||||||
{
|
|
||||||
public string? id { get; set; }
|
|
||||||
|
|
||||||
public int? alterId { get; set; }
|
|
||||||
|
|
||||||
public string? email { get; set; }
|
|
||||||
|
|
||||||
public string? security { get; set; }
|
|
||||||
|
|
||||||
public string? encryption { get; set; }
|
|
||||||
|
|
||||||
public string? flow { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Sniffing4Ray
|
public class Sniffing4Ray
|
||||||
{
|
{
|
||||||
public bool enabled { get; set; }
|
public bool enabled { get; set; }
|
||||||
@@ -137,10 +120,6 @@ public class Outbounds4Ray
|
|||||||
|
|
||||||
public class Outboundsettings4Ray
|
public class Outboundsettings4Ray
|
||||||
{
|
{
|
||||||
public List<VnextItem4Ray>? vnext { get; set; }
|
|
||||||
|
|
||||||
public List<ServersItem4Ray>? servers { get; set; }
|
|
||||||
|
|
||||||
public Response4Ray? response { get; set; }
|
public Response4Ray? response { get; set; }
|
||||||
|
|
||||||
public int? userLevel { get; set; }
|
public int? userLevel { get; set; }
|
||||||
@@ -174,29 +153,16 @@ public class Outboundsettings4Ray
|
|||||||
public int? version { get; set; }
|
public int? version { get; set; }
|
||||||
|
|
||||||
public List<string>? remoteDNS { get; set; }
|
public List<string>? remoteDNS { get; set; }
|
||||||
}
|
|
||||||
|
|
||||||
public class WireguardPeer4Ray
|
public string? id { get; set; }
|
||||||
{
|
|
||||||
public string endpoint { get; set; }
|
|
||||||
public string publicKey { get; set; }
|
|
||||||
public string? preSharedKey { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class VnextItem4Ray
|
public int? alterId { get; set; }
|
||||||
{
|
|
||||||
public string address { get; set; }
|
|
||||||
|
|
||||||
public int port { get; set; }
|
public string? security { get; set; }
|
||||||
|
|
||||||
public List<UsersItem4Ray> users { get; set; }
|
public string? encryption { get; set; }
|
||||||
}
|
|
||||||
|
|
||||||
public class ServersItem4Ray
|
public string? flow { get; set; }
|
||||||
{
|
|
||||||
public string email { get; set; }
|
|
||||||
|
|
||||||
public string address { get; set; }
|
|
||||||
|
|
||||||
public string? method { get; set; }
|
public string? method { get; set; }
|
||||||
|
|
||||||
@@ -204,24 +170,14 @@ public class ServersItem4Ray
|
|||||||
|
|
||||||
public string? password { get; set; }
|
public string? password { get; set; }
|
||||||
|
|
||||||
public int port { get; set; }
|
|
||||||
|
|
||||||
public int? level { get; set; }
|
|
||||||
|
|
||||||
public string flow { get; set; }
|
|
||||||
|
|
||||||
public bool? uot { get; set; }
|
public bool? uot { get; set; }
|
||||||
|
|
||||||
public List<SocksUsersItem4Ray> users { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SocksUsersItem4Ray
|
public class WireguardPeer4Ray
|
||||||
{
|
{
|
||||||
public string user { get; set; }
|
public string endpoint { get; set; }
|
||||||
|
public string publicKey { get; set; }
|
||||||
public string pass { get; set; }
|
public string? preSharedKey { get; set; }
|
||||||
|
|
||||||
public int? level { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Mux4Ray
|
public class Mux4Ray
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ public class ClashConnectionModel
|
|||||||
public double Time { get; set; }
|
public double Time { get; set; }
|
||||||
public string? Elapsed { get; set; }
|
public string? Elapsed { get; set; }
|
||||||
public string? Chain { get; set; }
|
public string? Chain { get; set; }
|
||||||
|
public string? ProcessPath { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,37 @@
|
|||||||
namespace ServiceLib.Models.Dto;
|
namespace ServiceLib.Models.Dto;
|
||||||
|
|
||||||
public class ClashConnections
|
public record ClashConnections
|
||||||
{
|
{
|
||||||
public ulong downloadTotal { get; set; }
|
public ulong downloadTotal { get; init; }
|
||||||
public ulong uploadTotal { get; set; }
|
public ulong uploadTotal { get; init; }
|
||||||
public List<ConnectionItem>? connections { get; set; }
|
public List<ConnectionItem>? connections { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ConnectionItem
|
public record ConnectionItem
|
||||||
{
|
{
|
||||||
public string? id { get; set; }
|
public string? id { get; init; }
|
||||||
public MetadataItem? metadata { get; set; }
|
public MetadataItem? metadata { get; init; }
|
||||||
public ulong upload { get; set; }
|
public ulong upload { get; init; }
|
||||||
public ulong download { get; set; }
|
public ulong download { get; init; }
|
||||||
public DateTime start { get; set; }
|
public DateTime start { get; init; }
|
||||||
public List<string>? chains { get; set; }
|
public List<string>? chains { get; init; }
|
||||||
public string? rule { get; set; }
|
public string? rule { get; init; }
|
||||||
public string? rulePayload { get; set; }
|
public string? rulePayload { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MetadataItem
|
public record MetadataItem
|
||||||
{
|
{
|
||||||
public string? network { get; set; }
|
public string? network { get; init; }
|
||||||
public string? type { get; set; }
|
public string? type { get; init; }
|
||||||
public string? sourceIP { get; set; }
|
public string? sourceIP { get; init; }
|
||||||
public string? destinationIP { get; set; }
|
public string? destinationIP { get; init; }
|
||||||
public string? sourcePort { get; set; }
|
public string? sourcePort { get; init; }
|
||||||
public string? destinationPort { get; set; }
|
public string? destinationPort { get; init; }
|
||||||
public string? host { get; set; }
|
public string? host { get; init; }
|
||||||
public string? nsMode { get; set; }
|
public string? nsMode { get; init; }
|
||||||
public object? uid { get; set; }
|
public object? uid { get; init; }
|
||||||
public string? process { get; set; }
|
public string? process { get; init; }
|
||||||
public string? processPath { get; set; }
|
public string? processPath { get; init; }
|
||||||
public string? remoteDestination { get; set; }
|
public string? remoteDestination { get; init; }
|
||||||
|
public string? sniffHost { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ServiceLib.Models.Dto;
|
||||||
|
|
||||||
|
public sealed record LanguageOption(string Value, string Display);
|
||||||
@@ -16,6 +16,8 @@ public class SubItem
|
|||||||
|
|
||||||
public string UserAgent { get; set; } = string.Empty;
|
public string UserAgent { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? RequestHeaders { get; set; }
|
||||||
|
|
||||||
public int Sort { get; set; }
|
public int Sort { get; set; }
|
||||||
|
|
||||||
public string? Filter { get; set; }
|
public string? Filter { get; set; }
|
||||||
|
|||||||
Generated
+45
-9
@@ -555,6 +555,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 HTTP headers (JSON) 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string LvRequestHeaders {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LvRequestHeaders", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Type 的本地化字符串。
|
/// 查找类似 Type 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2634,6 +2643,24 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Invalid HTTP headers. Use a JSON object with unique header names and string values. Header names and values must not contain line breaks. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string SubRequestHeadersInvalid {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SubRequestHeadersInvalid", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Optional. Example: {"X-hwid": "my_device"}. Values override default headers for all URLs in this group, including the subscription conversion service. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string SubRequestHeadersTips {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SubRequestHeadersTips", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 For group please leave blank here 的本地化字符串。
|
/// 查找类似 For group please leave blank here 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -4239,15 +4266,6 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查找类似 Update via proxy 的本地化字符串。
|
|
||||||
/// </summary>
|
|
||||||
public static string TbSettingsEnableUpdateViaProxy {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("TbSettingsEnableUpdateViaProxy", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Enable sorting Configurations by drag-n-drop (requires restart) 的本地化字符串。
|
/// 查找类似 Enable sorting Configurations by drag-n-drop (requires restart) 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -4284,6 +4302,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Update via proxy 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsEnableUpdateViaProxy {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsEnableUpdateViaProxy", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Exception 的本地化字符串。
|
/// 查找类似 Exception 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -4995,6 +5022,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Process 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSortingProcess {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSortingProcess", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Time 的本地化字符串。
|
/// 查找类似 Time 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1887,4 +1887,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbSettingsMux4Ray" xml:space="preserve">
|
<data name="TbSettingsMux4Ray" xml:space="preserve">
|
||||||
<value>Xray Mux setting</value>
|
<value>Xray Mux setting</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LvRequestHeaders" xml:space="preserve">
|
||||||
|
<value>HTTP headers (JSON)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubRequestHeadersTips" xml:space="preserve">
|
||||||
|
<value>Optional. Example: {"X-hwid": "my_device"}. Values override default headers for all URLs in this group, including the subscription conversion service.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubRequestHeadersInvalid" xml:space="preserve">
|
||||||
|
<value>Invalid HTTP headers. Use a JSON object with unique header names and string values. Header names and values must not contain line breaks.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSortingProcess" xml:space="preserve">
|
||||||
|
<value>Process</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1879,4 +1879,16 @@
|
|||||||
<data name="TbSettingsMux4Ray" xml:space="preserve">
|
<data name="TbSettingsMux4Ray" xml:space="preserve">
|
||||||
<value>Xray Mux 设置</value>
|
<value>Xray Mux 设置</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LvRequestHeaders" xml:space="preserve">
|
||||||
|
<value>HTTP 请求头 (JSON)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubRequestHeadersTips" xml:space="preserve">
|
||||||
|
<value>可选,示例:{"X-hwid": "my_device"}。自定义值覆盖默认请求头,应用于本组所有订阅地址(包括订阅转换服务)。</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubRequestHeadersInvalid" xml:space="preserve">
|
||||||
|
<value>HTTP 请求头无效。请使用 JSON 对象,键名不能重复,值必须为字符串;请求头名称和值不能包含换行符。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSortingProcess" xml:space="preserve">
|
||||||
|
<value>进程</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1020,6 +1020,15 @@
|
|||||||
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
|
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
|
||||||
<value>sing-box Mux 多路復用協定</value>
|
<value>sing-box Mux 多路復用協定</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsMux4RayConcurrency" xml:space="preserve">
|
||||||
|
<value>Xray Mux 並行數</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsMux4RayXudpConcurrency" xml:space="preserve">
|
||||||
|
<value>Xray Mux XUDP 並行數</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsMux4RayXudpProxyUDP443" xml:space="preserve">
|
||||||
|
<value>Xray Mux XUDP proxy UDP443</value>
|
||||||
|
</data>
|
||||||
<data name="TbRoutingRuleProcess" xml:space="preserve">
|
<data name="TbRoutingRuleProcess" xml:space="preserve">
|
||||||
<value>行程 (Linux/Windows)</value>
|
<value>行程 (Linux/Windows)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1864,4 +1873,31 @@
|
|||||||
<data name="LvCustomCoreType" xml:space="preserve">
|
<data name="LvCustomCoreType" xml:space="preserve">
|
||||||
<value>自訂設定核心</value>
|
<value>自訂設定核心</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbXrayOnly" xml:space="preserve">
|
||||||
|
<value>僅 Xray</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbBlockAAAAQueries" xml:space="preserve">
|
||||||
|
<value>封鎖 AAAA 查詢</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
|
||||||
|
<value>啟用後將封鎖 IPv6 查詢</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbDNS" xml:space="preserve">
|
||||||
|
<value>DNS</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsMux4Ray" xml:space="preserve">
|
||||||
|
<value>Xray Mux 設定</value>
|
||||||
|
</data>
|
||||||
|
<data name="LvRequestHeaders" xml:space="preserve">
|
||||||
|
<value>HTTP 請求標頭 (JSON)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubRequestHeadersTips" xml:space="preserve">
|
||||||
|
<value>可選,範例:{"X-hwid": "my_device"}。自訂值會覆寫預設標頭,套用於本組所有訂閱位址(包括訂閱轉換服務)。</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubRequestHeadersInvalid" xml:space="preserve">
|
||||||
|
<value>HTTP 請求標頭無效。請使用 JSON 物件,鍵名不能重複,值必須為字串;標頭名稱和值不能包含換行字元。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSortingProcess" xml:space="preserve">
|
||||||
|
<value>行程</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -2,28 +2,14 @@
|
|||||||
"tag": "proxy",
|
"tag": "proxy",
|
||||||
"protocol": "vmess",
|
"protocol": "vmess",
|
||||||
"settings": {
|
"settings": {
|
||||||
"vnext": [
|
"address": "v2ray.cool",
|
||||||
{
|
"port": 10086,
|
||||||
"address": "v2ray.cool",
|
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||||
"port": 10086,
|
"security": "auto",
|
||||||
"users": [
|
"method": "chacha20",
|
||||||
{
|
"ota": false,
|
||||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
"password": "123456",
|
||||||
"security": "auto"
|
"level": 1
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"address": "v2ray.cool",
|
|
||||||
"method": "chacha20",
|
|
||||||
"ota": false,
|
|
||||||
"password": "123456",
|
|
||||||
"port": 10086,
|
|
||||||
"level": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"streamSettings": {
|
"streamSettings": {
|
||||||
"network": "tcp"
|
"network": "tcp"
|
||||||
|
|||||||
@@ -62,6 +62,9 @@
|
|||||||
<DesignTime>True</DesignTime>
|
<DesignTime>True</DesignTime>
|
||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<EmbeddedResource Update="Resx\ResUI.az.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Update="Resx\ResUI.fa.resx">
|
<EmbeddedResource Update="Resx\ResUI.fa.resx">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
|||||||
@@ -282,9 +282,8 @@ public partial class CoreConfigV2rayService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address
|
var outboundAddress = outbound.settings?.address?.ToString()
|
||||||
?? outbound.settings?.vnext?.FirstOrDefault()?.address
|
?? outbound.settings?.peers?.FirstOrDefault()?.endpoint
|
||||||
?? outbound.settings?.address?.ToString()
|
|
||||||
?? string.Empty;
|
?? string.Empty;
|
||||||
|
|
||||||
if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase))
|
if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|||||||
@@ -69,166 +69,88 @@ public partial class CoreConfigV2rayService
|
|||||||
{
|
{
|
||||||
var protocolExtra = _node.GetProtocolExtra();
|
var protocolExtra = _node.GetProtocolExtra();
|
||||||
var muxEnabled = _node.MuxEnabled ?? false;
|
var muxEnabled = _node.MuxEnabled ?? false;
|
||||||
|
var outboundSettings = outbound.settings;
|
||||||
switch (_node.ConfigType)
|
switch (_node.ConfigType)
|
||||||
{
|
{
|
||||||
case EConfigType.VMess:
|
case EConfigType.VMess:
|
||||||
{
|
{
|
||||||
VnextItem4Ray vnextItem;
|
outboundSettings.address = _node.Address;
|
||||||
if (outbound.settings.vnext.Count <= 0)
|
outboundSettings.port = _node.Port;
|
||||||
{
|
outboundSettings.id = _node.Password;
|
||||||
vnextItem = new VnextItem4Ray();
|
outboundSettings.alterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0;
|
||||||
outbound.settings.vnext.Add(vnextItem);
|
outboundSettings.email = Global.UserEMail;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
vnextItem = outbound.settings.vnext.First();
|
|
||||||
}
|
|
||||||
vnextItem.address = _node.Address;
|
|
||||||
vnextItem.port = _node.Port;
|
|
||||||
|
|
||||||
UsersItem4Ray usersItem;
|
|
||||||
if (vnextItem.users.Count <= 0)
|
|
||||||
{
|
|
||||||
usersItem = new UsersItem4Ray();
|
|
||||||
vnextItem.users.Add(usersItem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
usersItem = vnextItem.users.First();
|
|
||||||
}
|
|
||||||
|
|
||||||
usersItem.id = _node.Password;
|
|
||||||
usersItem.alterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0;
|
|
||||||
usersItem.email = Global.UserEMail;
|
|
||||||
if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity))
|
if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity))
|
||||||
{
|
{
|
||||||
usersItem.security = protocolExtra.VmessSecurity;
|
outboundSettings.security = protocolExtra.VmessSecurity;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
usersItem.security = Global.DefaultSecurity;
|
outboundSettings.security = Global.DefaultSecurity;
|
||||||
}
|
}
|
||||||
|
|
||||||
FillOutboundMux(outbound, muxEnabled, muxEnabled);
|
FillOutboundMux(outbound, muxEnabled, muxEnabled);
|
||||||
|
|
||||||
outbound.settings.servers = null;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EConfigType.Shadowsocks:
|
case EConfigType.Shadowsocks:
|
||||||
{
|
{
|
||||||
ServersItem4Ray serversItem;
|
outboundSettings.address = _node.Address;
|
||||||
if (outbound.settings.servers.Count <= 0)
|
outboundSettings.port = _node.Port;
|
||||||
{
|
outboundSettings.password = _node.Password;
|
||||||
serversItem = new ServersItem4Ray();
|
outboundSettings.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod)
|
||||||
outbound.settings.servers.Add(serversItem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
serversItem = outbound.settings.servers.First();
|
|
||||||
}
|
|
||||||
serversItem.address = _node.Address;
|
|
||||||
serversItem.port = _node.Port;
|
|
||||||
serversItem.password = _node.Password;
|
|
||||||
serversItem.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod)
|
|
||||||
? protocolExtra.SsMethod : "none";
|
? protocolExtra.SsMethod : "none";
|
||||||
serversItem.uot = protocolExtra.Uot == true ? true : null;
|
outboundSettings.uot = protocolExtra.Uot == true ? true : null;
|
||||||
|
|
||||||
serversItem.ota = false;
|
outboundSettings.ota = false;
|
||||||
serversItem.level = 1;
|
outboundSettings.level = 1;
|
||||||
|
|
||||||
FillOutboundMux(outbound);
|
FillOutboundMux(outbound);
|
||||||
|
|
||||||
outbound.settings.vnext = null;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EConfigType.SOCKS:
|
case EConfigType.SOCKS:
|
||||||
{
|
{
|
||||||
ServersItem4Ray serversItem;
|
outboundSettings.address = _node.Address;
|
||||||
if (outbound.settings.servers.Count <= 0)
|
outboundSettings.port = _node.Port;
|
||||||
{
|
|
||||||
serversItem = new ServersItem4Ray();
|
|
||||||
outbound.settings.servers.Add(serversItem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
serversItem = outbound.settings.servers.First();
|
|
||||||
}
|
|
||||||
serversItem.address = _node.Address;
|
|
||||||
serversItem.port = _node.Port;
|
|
||||||
serversItem.method = null;
|
|
||||||
serversItem.password = null;
|
|
||||||
|
|
||||||
if (_node.Username.IsNotEmpty()
|
if (_node.Username.IsNotEmpty()
|
||||||
&& _node.Password.IsNotEmpty())
|
&& _node.Password.IsNotEmpty())
|
||||||
{
|
{
|
||||||
SocksUsersItem4Ray socksUsersItem = new()
|
outboundSettings.user = _node.Username;
|
||||||
{
|
outboundSettings.pass = _node.Password;
|
||||||
user = _node.Username ?? "",
|
outboundSettings.level = 1;
|
||||||
pass = _node.Password,
|
outboundSettings.email = Global.UserEMail;
|
||||||
level = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
serversItem.users = new List<SocksUsersItem4Ray>() { socksUsersItem };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FillOutboundMux(outbound);
|
FillOutboundMux(outbound);
|
||||||
|
|
||||||
outbound.settings.vnext = null;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EConfigType.HTTP:
|
case EConfigType.HTTP:
|
||||||
{
|
{
|
||||||
outbound.settings.address = _node.Address;
|
outboundSettings.address = _node.Address;
|
||||||
outbound.settings.port = _node.Port;
|
outboundSettings.port = _node.Port;
|
||||||
|
|
||||||
if (protocolExtra.HttpHeaders.IsNotEmpty())
|
if (protocolExtra.HttpHeaders.IsNotEmpty())
|
||||||
{
|
{
|
||||||
outbound.settings.headers = JsonUtils.ParseJson(protocolExtra.HttpHeaders);
|
outboundSettings.headers = JsonUtils.ParseJson(protocolExtra.HttpHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_node.Username.IsNotEmpty()
|
if (_node.Username.IsNotEmpty()
|
||||||
&& _node.Password.IsNotEmpty())
|
&& _node.Password.IsNotEmpty())
|
||||||
{
|
{
|
||||||
outbound.settings.user = _node.Username;
|
outboundSettings.user = _node.Username;
|
||||||
outbound.settings.pass = _node.Password;
|
outboundSettings.pass = _node.Password;
|
||||||
outbound.settings.level = 1;
|
outboundSettings.level = 1;
|
||||||
outbound.settings.email = Global.UserEMail;
|
outboundSettings.email = Global.UserEMail;
|
||||||
}
|
}
|
||||||
|
|
||||||
FillOutboundMux(outbound);
|
FillOutboundMux(outbound);
|
||||||
|
|
||||||
outbound.settings.vnext = null;
|
|
||||||
outbound.settings.servers = null;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EConfigType.VLESS:
|
case EConfigType.VLESS:
|
||||||
{
|
{
|
||||||
VnextItem4Ray vnextItem;
|
outboundSettings.address = _node.Address;
|
||||||
if (outbound.settings.vnext?.Count <= 0)
|
outboundSettings.port = _node.Port;
|
||||||
{
|
outboundSettings.id = _node.Password;
|
||||||
vnextItem = new VnextItem4Ray();
|
outboundSettings.email = Global.UserEMail;
|
||||||
outbound.settings.vnext.Add(vnextItem);
|
outboundSettings.encryption = protocolExtra.VlessEncryption;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
vnextItem = outbound.settings.vnext.First();
|
|
||||||
}
|
|
||||||
vnextItem.address = _node.Address;
|
|
||||||
vnextItem.port = _node.Port;
|
|
||||||
|
|
||||||
UsersItem4Ray usersItem;
|
|
||||||
if (vnextItem.users.Count <= 0)
|
|
||||||
{
|
|
||||||
usersItem = new UsersItem4Ray();
|
|
||||||
vnextItem.users.Add(usersItem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
usersItem = vnextItem.users.First();
|
|
||||||
}
|
|
||||||
usersItem.id = _node.Password;
|
|
||||||
usersItem.email = Global.UserEMail;
|
|
||||||
usersItem.encryption = protocolExtra.VlessEncryption;
|
|
||||||
|
|
||||||
if (protocolExtra.Flow.IsNullOrEmpty())
|
if (protocolExtra.Flow.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -236,46 +158,28 @@ public partial class CoreConfigV2rayService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
usersItem.flow = protocolExtra.Flow;
|
outboundSettings.flow = protocolExtra.Flow;
|
||||||
FillOutboundMux(outbound, false, muxEnabled);
|
FillOutboundMux(outbound, false, muxEnabled);
|
||||||
}
|
}
|
||||||
outbound.settings.servers = null;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EConfigType.Trojan:
|
case EConfigType.Trojan:
|
||||||
{
|
{
|
||||||
ServersItem4Ray serversItem;
|
outboundSettings.address = _node.Address;
|
||||||
if (outbound.settings.servers.Count <= 0)
|
outboundSettings.port = _node.Port;
|
||||||
{
|
outboundSettings.password = _node.Password;
|
||||||
serversItem = new ServersItem4Ray();
|
|
||||||
outbound.settings.servers.Add(serversItem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
serversItem = outbound.settings.servers.First();
|
|
||||||
}
|
|
||||||
serversItem.address = _node.Address;
|
|
||||||
serversItem.port = _node.Port;
|
|
||||||
serversItem.password = _node.Password;
|
|
||||||
|
|
||||||
serversItem.ota = false;
|
outboundSettings.ota = false;
|
||||||
serversItem.level = 1;
|
outboundSettings.level = 1;
|
||||||
|
|
||||||
FillOutboundMux(outbound);
|
FillOutboundMux(outbound);
|
||||||
|
|
||||||
outbound.settings.vnext = null;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EConfigType.Hysteria2:
|
case EConfigType.Hysteria2:
|
||||||
{
|
{
|
||||||
outbound.settings = new()
|
outboundSettings.address = _node.Address;
|
||||||
{
|
outboundSettings.port = _node.Port;
|
||||||
version = 2,
|
outboundSettings.version = 2;
|
||||||
address = _node.Address,
|
|
||||||
port = _node.Port,
|
|
||||||
vnext = null,
|
|
||||||
servers = null,
|
|
||||||
};
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EConfigType.WireGuard:
|
case EConfigType.WireGuard:
|
||||||
@@ -301,8 +205,6 @@ public partial class CoreConfigV2rayService
|
|||||||
peers = [peer],
|
peers = [peer],
|
||||||
};
|
};
|
||||||
outbound.settings = setting;
|
outbound.settings = setting;
|
||||||
outbound.settings.vnext = null;
|
|
||||||
outbound.settings.servers = null;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public class DownloadService
|
|||||||
|
|
||||||
public string? AcceptHeader { get; init; }
|
public string? AcceptHeader { get; init; }
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string>? RequestHeaders { get; init; }
|
||||||
|
|
||||||
private static readonly string _tag = "DownloadService";
|
private static readonly string _tag = "DownloadService";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -236,6 +238,7 @@ public class DownloadService
|
|||||||
{
|
{
|
||||||
Proxy = webProxy,
|
Proxy = webProxy,
|
||||||
UseProxy = webProxy != null,
|
UseProxy = webProxy != null,
|
||||||
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
ConnectTimeout = TimeSpan.FromSeconds(connectTimeout)
|
ConnectTimeout = TimeSpan.FromSeconds(connectTimeout)
|
||||||
};
|
};
|
||||||
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
||||||
@@ -245,7 +248,7 @@ public class DownloadService
|
|||||||
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var client = new HttpClient(handler)
|
using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders))
|
||||||
{
|
{
|
||||||
Timeout = Timeout.InfiniteTimeSpan
|
Timeout = Timeout.InfiniteTimeSpan
|
||||||
};
|
};
|
||||||
@@ -296,7 +299,7 @@ public class DownloadService
|
|||||||
{
|
{
|
||||||
userAgent = Utils.GetVersion(false);
|
userAgent = Utils.GetVersion(false);
|
||||||
}
|
}
|
||||||
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout);
|
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout, RequestHeaders, AcceptHeader);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -63,8 +63,20 @@ 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 =
|
if (item.metadata == null)
|
||||||
$"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}";
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var dest = item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host;
|
||||||
|
var hostSb = new StringBuilder();
|
||||||
|
hostSb.Append(dest);
|
||||||
|
hostSb.Append($":{item.metadata.destinationPort}");
|
||||||
|
if (!string.IsNullOrEmpty(item.metadata.sniffHost) &&
|
||||||
|
dest?.Equals(item.metadata.sniffHost, StringComparison.OrdinalIgnoreCase) == false)
|
||||||
|
{
|
||||||
|
hostSb.Append($" ({item.metadata.sniffHost})");
|
||||||
|
}
|
||||||
|
var host = hostSb.ToString();
|
||||||
if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter))
|
if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -79,6 +91,7 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
|
|||||||
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 ?? [])}",
|
||||||
|
ProcessPath = item.metadata.processPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
lstModel.Add(model);
|
lstModel.Add(model);
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
ProfilesViewModel.RefreshServersRequested
|
ProfilesViewModel.RefreshServersRequested
|
||||||
.AsObservable()
|
.AsObservable()
|
||||||
.ObserveOn(RxSchedulers.MainThreadScheduler)
|
.ObserveOn(RxSchedulers.MainThreadScheduler)
|
||||||
.SubscribeAsync(async _ => await RefreshServers());
|
.SubscribeAsync(async _ => await RefreshServersDispatcherAsync());
|
||||||
|
|
||||||
var vmReloadRequestedList = new List<IObservable<RxVoid>>
|
var vmReloadRequestedList = new List<IObservable<RxVoid>>
|
||||||
{
|
{
|
||||||
@@ -400,6 +400,8 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
|
|
||||||
#region Servers && Groups
|
#region Servers && Groups
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _refreshServersSemaphore = new(1, 1);
|
||||||
|
|
||||||
private async Task RefreshServers()
|
private async Task RefreshServers()
|
||||||
{
|
{
|
||||||
await ProfilesViewModel.RefreshServersBiz();
|
await ProfilesViewModel.RefreshServersBiz();
|
||||||
@@ -411,13 +413,21 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
private async Task RefreshServersDispatcherAsync()
|
private async Task RefreshServersDispatcherAsync()
|
||||||
{
|
{
|
||||||
//await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
|
//await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
|
||||||
await Signal.FromAsync(async () =>
|
await _refreshServersSemaphore.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Signal.FromAsync(async () =>
|
||||||
{
|
{
|
||||||
await RefreshServers();
|
await RefreshServers();
|
||||||
return RxVoid.Default;
|
return RxVoid.Default;
|
||||||
})
|
})
|
||||||
.SubscribeOn(RxSchedulers.MainThreadScheduler)
|
.SubscribeOn(RxSchedulers.MainThreadScheduler)
|
||||||
.ToTask();
|
.ToTask();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_refreshServersSemaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshSubscriptions()
|
private async Task RefreshSubscriptions()
|
||||||
@@ -739,7 +749,10 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
RxSchedulers.MainThreadScheduler.Schedule(() =>
|
RxSchedulers.MainThreadScheduler.Schedule(() =>
|
||||||
{
|
{
|
||||||
ShowClashUI = showClashUI;
|
ShowClashUI = showClashUI;
|
||||||
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0;
|
if (!showClashUI || TabMainSelectedIndex < 0)
|
||||||
|
{
|
||||||
|
TabMainSelectedIndex = 0;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
|
|||||||
// React to ConfigType filter changes
|
// React to ConfigType filter changes
|
||||||
this.WhenAnyValue(x => x.FilterExclude)
|
this.WhenAnyValue(x => x.FilterExclude)
|
||||||
.Skip(1)
|
.Skip(1)
|
||||||
.SubscribeAsync(async _ => await RefreshServersBiz());
|
.SubscribeAsync(async _ => await RefreshServers());
|
||||||
|
|
||||||
this.WhenAnyValue(x => x.FilterConfigTypes)
|
this.WhenAnyValue(x => x.FilterConfigTypes)
|
||||||
.Skip(1)
|
.Skip(1)
|
||||||
.SubscribeAsync(async _ => await RefreshServersBiz());
|
.SubscribeAsync(async _ => await RefreshServers());
|
||||||
|
|
||||||
#endregion WhenAnyValue && ReactiveCommand
|
#endregion WhenAnyValue && ReactiveCommand
|
||||||
|
|
||||||
@@ -146,7 +146,13 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
|
|||||||
|
|
||||||
public async Task RefreshServers()
|
public async Task RefreshServers()
|
||||||
{
|
{
|
||||||
await RefreshServersBiz();
|
await Signal.FromAsync(async () =>
|
||||||
|
{
|
||||||
|
await RefreshServersBiz();
|
||||||
|
return RxVoid.Default;
|
||||||
|
})
|
||||||
|
.SubscribeOn(RxSchedulers.MainThreadScheduler)
|
||||||
|
.ToTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshServersBiz()
|
private async Task RefreshServersBiz()
|
||||||
@@ -176,7 +182,10 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
|
|||||||
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
|
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
|
||||||
{
|
{
|
||||||
var lstModel = await AppManager.Instance.ProfileModels(_subIndexId, filter);
|
var lstModel = await AppManager.Instance.ProfileModels(_subIndexId, filter);
|
||||||
|
var lstProfileExs = await ProfileExManager.Instance.GetProfileExs();
|
||||||
lstModel = (from t in lstModel
|
lstModel = (from t in lstModel
|
||||||
|
join t3 in lstProfileExs on t.IndexId equals t3.IndexId into t3b
|
||||||
|
from t33 in t3b.DefaultIfEmpty()
|
||||||
select new ProfileItemModel
|
select new ProfileItemModel
|
||||||
{
|
{
|
||||||
IndexId = t.IndexId,
|
IndexId = t.IndexId,
|
||||||
@@ -190,6 +199,12 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
|
|||||||
Subid = t.Subid,
|
Subid = t.Subid,
|
||||||
SubRemarks = t.SubRemarks,
|
SubRemarks = t.SubRemarks,
|
||||||
IsActive = t.IndexId == _config.IndexId,
|
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,
|
||||||
|
IpInfo = t33?.IpInfo ?? string.Empty,
|
||||||
}).OrderBy(t => t.Sort).ToList();
|
}).OrderBy(t => t.Sort).ToList();
|
||||||
|
|
||||||
// Apply ConfigType filter (include or exclude)
|
// Apply ConfigType filter (include or exclude)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
|
|
||||||
#region private prop
|
#region private prop
|
||||||
|
|
||||||
private List<ProfileItem> _lstProfile;
|
|
||||||
private string _serverFilter = string.Empty;
|
private string _serverFilter = string.Empty;
|
||||||
private readonly Dictionary<string, bool> _dicHeaderSort = new();
|
private readonly Dictionary<string, bool> _dicHeaderSort = new();
|
||||||
private SpeedtestService? _speedtestService;
|
private SpeedtestService? _speedtestService;
|
||||||
@@ -362,7 +361,6 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
public async Task RefreshServersBiz()
|
public async Task RefreshServersBiz()
|
||||||
{
|
{
|
||||||
var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter);
|
var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter);
|
||||||
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
|
|
||||||
|
|
||||||
ProfileItems.ReplaceRange(lstModel ?? []);
|
ProfileItems.ReplaceRange(lstModel ?? []);
|
||||||
if (lstModel?.Count > 0)
|
if (lstModel?.Count > 0)
|
||||||
@@ -677,19 +675,15 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
|
|
||||||
public async Task MoveServer(EMove eMove)
|
public async Task MoveServer(EMove eMove)
|
||||||
{
|
{
|
||||||
var item = _lstProfile.FirstOrDefault(t => t.IndexId == SelectedProfile.IndexId);
|
var lstProfile = ProfileItems?.Select(t => t.IndexId).ToList() ?? [];
|
||||||
if (item is null)
|
var index = lstProfile.IndexOf(SelectedProfile.IndexId);
|
||||||
|
if (index < 0)
|
||||||
{
|
{
|
||||||
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
|
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = _lstProfile.IndexOf(item);
|
if (await ConfigHandler.MoveServer(_config, lstProfile, index, eMove) == 0)
|
||||||
if (index < 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (await ConfigHandler.MoveServer(_config, _lstProfile, index, eMove) == 0)
|
|
||||||
{
|
{
|
||||||
await RefreshServers();
|
await RefreshServers();
|
||||||
}
|
}
|
||||||
@@ -700,7 +694,8 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
var targetIndex = ProfileItems.IndexOf(targetItem);
|
var targetIndex = ProfileItems.IndexOf(targetItem);
|
||||||
if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex)
|
if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex)
|
||||||
{
|
{
|
||||||
if (await ConfigHandler.MoveServer(_config, _lstProfile, startIndex, EMove.Position, targetIndex) == 0)
|
var lstProfile = ProfileItems?.Select(t => t.IndexId).ToList() ?? [];
|
||||||
|
if (await ConfigHandler.MoveServer(_config, lstProfile, startIndex, EMove.Position, targetIndex) == 0)
|
||||||
{
|
{
|
||||||
await RefreshServers();
|
await RefreshServers();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!HttpRequestHeadersHelper.TryParse(SelectedSource.RequestHeaders, out _))
|
||||||
|
{
|
||||||
|
NoticeManager.Instance.Enqueue(ResUI.SubRequestHeadersInvalid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null;
|
SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null;
|
||||||
SelectedSource.PrevProfile = PrevProfile;
|
SelectedSource.PrevProfile = PrevProfile;
|
||||||
SelectedSource.NextProfile = NextProfile;
|
SelectedSource.NextProfile = NextProfile;
|
||||||
|
|||||||
@@ -93,6 +93,11 @@
|
|||||||
Binding="{Binding Type}"
|
Binding="{Binding Type}"
|
||||||
Header="{x:Static resx:ResUI.TbSortingType}"
|
Header="{x:Static resx:ResUI.TbSortingType}"
|
||||||
Tag="Type" />
|
Tag="Type" />
|
||||||
|
<DataGridTextColumn
|
||||||
|
Width="100"
|
||||||
|
Binding="{Binding ProcessPath}"
|
||||||
|
Header="{x:Static resx:ResUI.TbSortingProcess}"
|
||||||
|
Tag="ProcessPath" />
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn
|
||||||
Width="100"
|
Width="100"
|
||||||
Binding="{Binding Elapsed}"
|
Binding="{Binding Elapsed}"
|
||||||
|
|||||||
@@ -447,19 +447,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
//// workaround
|
|
||||||
//Task.Run(async () =>
|
|
||||||
//{
|
|
||||||
// await Task.Delay(5000);
|
|
||||||
// Dispatcher.UIThread.Post(() =>
|
|
||||||
// {
|
|
||||||
// ViewModel?.TabMainSelectedIndex = 0;
|
|
||||||
// tabMain.SelectedIndex = 0;
|
|
||||||
// tabMain1.SelectedIndex = 0;
|
|
||||||
// tabMain2.SelectedIndex = 0;
|
|
||||||
// });
|
|
||||||
//});
|
|
||||||
|
|
||||||
RestoreUI();
|
RestoreUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
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:dd="clr-namespace:v2rayN.Desktop.DesignData"
|
xmlns:dd="clr-namespace:v2rayN.Desktop.DesignData"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@@ -15,6 +16,10 @@
|
|||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<conv:DelayColorConverter x:Key="DelayColorConverter" />
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
<DockPanel Margin="{StaticResource Margin8}">
|
<DockPanel Margin="{StaticResource Margin8}">
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
@@ -121,6 +126,26 @@
|
|||||||
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="Delay">
|
||||||
|
<DataGridTemplateColumn.Header>
|
||||||
|
<TextBlock Text="{x:Static resx:ResUI.LvTestDelay}" />
|
||||||
|
</DataGridTemplateColumn.Header>
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock
|
||||||
|
Margin="{StaticResource MarginLr8}"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{Binding Delay, Converter={StaticResource DelayColorConverter}}"
|
||||||
|
Text="{Binding Path=DelayVal, Mode=OneWay}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
<DataGridTextColumn
|
||||||
|
Width="100"
|
||||||
|
Binding="{Binding SpeedVal}"
|
||||||
|
Header="{x:Static resx:ResUI.LvTestSpeed}"
|
||||||
|
Tag="Speed" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|||||||
@@ -186,79 +186,101 @@
|
|||||||
Grid.Row="8"
|
Grid.Row="8"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Text="{x:Static resx:ResUI.LvRequestHeaders}" />
|
||||||
|
<StackPanel Grid.Row="8" Grid.Column="1">
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtRequestHeaders"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
MinLines="4"
|
||||||
|
MaxLines="8"
|
||||||
|
Classes="TextArea"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
Text="{x:Static resx:ResUI.SubRequestHeadersTips}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="9"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{x:Static resx:ResUI.LvSort}" />
|
Text="{x:Static resx:ResUI.LvSort}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtSort"
|
x:Name="txtSort"
|
||||||
Grid.Row="8"
|
Grid.Row="9"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="100"
|
Width="100"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left" />
|
HorizontalAlignment="Left" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="9"
|
Grid.Row="10"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{x:Static resx:ResUI.LvPrevProfile}" />
|
Text="{x:Static resx:ResUI.LvPrevProfile}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtPrevProfile"
|
x:Name="txtPrevProfile"
|
||||||
Grid.Row="9"
|
Grid.Row="10"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSelectPrevProfile"
|
x:Name="btnSelectPrevProfile"
|
||||||
Grid.Row="9"
|
Grid.Row="10"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
Content="{x:Static resx:ResUI.TbSelectProfile}" />
|
Content="{x:Static resx:ResUI.TbSelectProfile}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="10"
|
Grid.Row="11"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{x:Static resx:ResUI.LvNextProfile}" />
|
Text="{x:Static resx:ResUI.LvNextProfile}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtNextProfile"
|
x:Name="txtNextProfile"
|
||||||
Grid.Row="10"
|
Grid.Row="11"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSelectNextProfile"
|
x:Name="btnSelectNextProfile"
|
||||||
Grid.Row="10"
|
Grid.Row="11"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
Content="{x:Static resx:ResUI.TbSelectProfile}" />
|
Content="{x:Static resx:ResUI.TbSelectProfile}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="11"
|
Grid.Row="12"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{x:Static resx:ResUI.LvCustomCoreType}" />
|
Text="{x:Static resx:ResUI.LvCustomCoreType}" />
|
||||||
<ComboBox
|
<ComboBox
|
||||||
x:Name="cmbCustomCoreType"
|
x:Name="cmbCustomCoreType"
|
||||||
Grid.Row="11"
|
Grid.Row="12"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="200"
|
Width="200"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left" />
|
HorizontalAlignment="Left" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="12"
|
Grid.Row="13"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
|
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtPreSocksPort"
|
x:Name="txtPreSocksPort"
|
||||||
Grid.Row="12"
|
Grid.Row="13"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="200"
|
Width="200"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
@@ -267,14 +289,14 @@
|
|||||||
ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}" />
|
ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="13"
|
Grid.Row="14"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{x:Static resx:ResUI.LvMemo}" />
|
Text="{x:Static resx:ResUI.LvMemo}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtMemo"
|
x:Name="txtMemo"
|
||||||
Grid.Row="13"
|
Grid.Row="14"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
|||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SelectedSource.RequestHeaders, v => v.txtRequestHeaders.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.SelectedValue).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.SelectedValue).DisposeWith(disposables);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Avalonia.Data;
|
||||||
using v2rayN.Desktop.ViewModels;
|
using v2rayN.Desktop.ViewModels;
|
||||||
|
|
||||||
namespace v2rayN.Desktop.Views;
|
namespace v2rayN.Desktop.Views;
|
||||||
@@ -14,7 +15,9 @@ public partial class ThemeSettingView : ReactiveUserControl<ThemeSettingViewMode
|
|||||||
|
|
||||||
cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>();
|
cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>();
|
||||||
cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList();
|
cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList();
|
||||||
cmbCurrentLanguage.ItemsSource = Global.Languages;
|
cmbCurrentLanguage.ItemsSource = Global.LanguageOptions;
|
||||||
|
cmbCurrentLanguage.DisplayMemberBinding = new Binding(nameof(LanguageOption.Display));
|
||||||
|
cmbCurrentLanguage.SelectedValueBinding = new Binding(nameof(LanguageOption.Value));
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -99,6 +99,11 @@
|
|||||||
Binding="{Binding Type}"
|
Binding="{Binding Type}"
|
||||||
ExName="Type"
|
ExName="Type"
|
||||||
Header="{x:Static resx:ResUI.TbSortingType}" />
|
Header="{x:Static resx:ResUI.TbSortingType}" />
|
||||||
|
<base:MyDGTextColumn
|
||||||
|
Width="100"
|
||||||
|
Binding="{Binding ProcessPath}"
|
||||||
|
ExName="ProcessPath"
|
||||||
|
Header="{x:Static resx:ResUI.TbSortingProcess}" />
|
||||||
<base:MyDGTextColumn
|
<base:MyDGTextColumn
|
||||||
Width="100"
|
Width="100"
|
||||||
Binding="{Binding Elapsed}"
|
Binding="{Binding Elapsed}"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
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"
|
||||||
@@ -17,6 +18,10 @@
|
|||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<conv:DelayColorConverter x:Key="DelayColorConverter" />
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
<DockPanel Margin="{StaticResource Margin8}">
|
<DockPanel Margin="{StaticResource Margin8}">
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
@@ -147,6 +152,30 @@
|
|||||||
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="Delay"
|
||||||
|
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="Speed"
|
||||||
|
Header="{x:Static resx:ResUI.LvTestSpeed}">
|
||||||
|
<DataGridTextColumn.ElementStyle>
|
||||||
|
<Style TargetType="{x:Type TextBlock}">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||||
|
</Style>
|
||||||
|
</DataGridTextColumn.ElementStyle>
|
||||||
|
</base:MyDGTextColumn>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
@@ -230,12 +231,34 @@
|
|||||||
Grid.Row="8"
|
Grid.Row="8"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Text="{x:Static resx:ResUI.LvRequestHeaders}" />
|
||||||
|
<StackPanel Grid.Row="8" Grid.Column="1">
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtRequestHeaders"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
MinLines="4"
|
||||||
|
MaxLines="8"
|
||||||
|
Style="{StaticResource MyOutlinedTextBox}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
VerticalScrollBarVisibility="Auto" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
Text="{x:Static resx:ResUI.SubRequestHeadersTips}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="9"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Style="{StaticResource ToolbarTextBlock}"
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
Text="{x:Static resx:ResUI.LvSort}" />
|
Text="{x:Static resx:ResUI.LvSort}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtSort"
|
x:Name="txtSort"
|
||||||
Grid.Row="8"
|
Grid.Row="9"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="100"
|
Width="100"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
@@ -245,7 +268,7 @@
|
|||||||
Style="{StaticResource MyOutlinedTextBox}" />
|
Style="{StaticResource MyOutlinedTextBox}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="9"
|
Grid.Row="10"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -253,7 +276,7 @@
|
|||||||
Text="{x:Static resx:ResUI.LvPrevProfile}" />
|
Text="{x:Static resx:ResUI.LvPrevProfile}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtPrevProfile"
|
x:Name="txtPrevProfile"
|
||||||
Grid.Row="9"
|
Grid.Row="10"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -262,7 +285,7 @@
|
|||||||
Style="{StaticResource MyOutlinedTextBox}" />
|
Style="{StaticResource MyOutlinedTextBox}" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSelectPrevProfile"
|
x:Name="btnSelectPrevProfile"
|
||||||
Grid.Row="9"
|
Grid.Row="10"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -270,7 +293,7 @@
|
|||||||
Style="{StaticResource DefButton}" />
|
Style="{StaticResource DefButton}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="10"
|
Grid.Row="11"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -278,7 +301,7 @@
|
|||||||
Text="{x:Static resx:ResUI.LvNextProfile}" />
|
Text="{x:Static resx:ResUI.LvNextProfile}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtNextProfile"
|
x:Name="txtNextProfile"
|
||||||
Grid.Row="10"
|
Grid.Row="11"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -287,7 +310,7 @@
|
|||||||
Style="{StaticResource MyOutlinedTextBox}" />
|
Style="{StaticResource MyOutlinedTextBox}" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSelectNextProfile"
|
x:Name="btnSelectNextProfile"
|
||||||
Grid.Row="10"
|
Grid.Row="11"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -295,7 +318,7 @@
|
|||||||
Style="{StaticResource DefButton}" />
|
Style="{StaticResource DefButton}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="11"
|
Grid.Row="12"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -303,14 +326,14 @@
|
|||||||
Text="{x:Static resx:ResUI.LvCustomCoreType}" />
|
Text="{x:Static resx:ResUI.LvCustomCoreType}" />
|
||||||
<ComboBox
|
<ComboBox
|
||||||
x:Name="cmbCustomCoreType"
|
x:Name="cmbCustomCoreType"
|
||||||
Grid.Row="11"
|
Grid.Row="12"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
MaxDropDownHeight="1000"
|
MaxDropDownHeight="1000"
|
||||||
Style="{StaticResource MyOutlinedTextComboBox}" />
|
Style="{StaticResource MyOutlinedTextComboBox}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="12"
|
Grid.Row="13"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -318,7 +341,7 @@
|
|||||||
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
|
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtPreSocksPort"
|
x:Name="txtPreSocksPort"
|
||||||
Grid.Row="12"
|
Grid.Row="13"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
@@ -328,7 +351,7 @@
|
|||||||
ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" />
|
ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="13"
|
Grid.Row="14"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -336,7 +359,7 @@
|
|||||||
Text="{x:Static resx:ResUI.LvMemo}" />
|
Text="{x:Static resx:ResUI.LvMemo}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtMemo"
|
x:Name="txtMemo"
|
||||||
Grid.Row="13"
|
Grid.Row="14"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public partial class SubEditWindow
|
|||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SelectedSource.RequestHeaders, v => v.txtRequestHeaders.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.Text).DisposeWith(disposables);
|
||||||
|
|||||||
@@ -81,6 +81,8 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="120"
|
Width="120"
|
||||||
Margin="{StaticResource Margin8}"
|
Margin="{StaticResource Margin8}"
|
||||||
|
DisplayMemberPath="Display"
|
||||||
|
SelectedValuePath="Value"
|
||||||
Style="{StaticResource DefComboBox}" />
|
Style="{StaticResource DefComboBox}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public partial class ThemeSettingView
|
|||||||
|
|
||||||
cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>().Take(3).ToList();
|
cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>().Take(3).ToList();
|
||||||
cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList();
|
cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList();
|
||||||
cmbCurrentLanguage.ItemsSource = Global.Languages;
|
cmbCurrentLanguage.ItemsSource = Global.LanguageOptions;
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
@@ -22,7 +22,7 @@ public partial class ThemeSettingView
|
|||||||
this.OneWayBind(ViewModel, vm => vm.Swatches, v => v.cmbSwatches.ItemsSource).DisposeWith(disposables);
|
this.OneWayBind(ViewModel, vm => vm.Swatches, v => v.cmbSwatches.ItemsSource).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSwatch, v => v.cmbSwatches.SelectedItem).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSwatch, v => v.cmbSwatches.SelectedItem).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.CurrentFontSize, v => v.cmbCurrentFontSize.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.CurrentFontSize, v => v.cmbCurrentFontSize.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.CurrentLanguage, v => v.cmbCurrentLanguage.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.CurrentLanguage, v => v.cmbCurrentLanguage.SelectedValue).DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user