mirror of
https://github.com/2dust/v2rayN.git
synced 2026-08-15 11:42:05 +03:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ad2c46e6 | ||
|
|
230a2f6773 | ||
|
|
e7cc4c832e | ||
|
|
fb7033aab5 | ||
|
|
b6cc3913ac | ||
|
|
bc06a5dc21 | ||
|
|
9c1951178c | ||
|
|
b3df42b9ba | ||
|
|
e01717d832 | ||
|
|
8b4063b44b | ||
|
|
3136f573dd | ||
|
|
e101b1d7b0 |
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>7.24.6</Version>
|
||||
<Version>7.24.7</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -251,4 +251,12 @@ internal static class CoreConfigTestFactory
|
||||
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
|
||||
return config;
|
||||
}
|
||||
|
||||
public static Config CreateConfigWithTun(ECoreType coreType, bool enableIPv6Address)
|
||||
{
|
||||
var config = CreateConfig(coreType);
|
||||
config.TunModeItem.EnableTun = true;
|
||||
config.TunModeItem.EnableIPv6Address = enableIPv6Address;
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,85 @@ public class CoreConfigSingboxServiceTests
|
||||
cfg.inbounds.Should().Contain(i => i.type == "tun");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules()
|
||||
{
|
||||
// The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast).
|
||||
// They are deserialized into List<Rule4Sbox>, so a schema mismatch in the
|
||||
// embedded template makes JsonUtils.Deserialize return null and silently
|
||||
// drops every one of them.
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
config.TunModeItem.EnableTun = true;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||
{
|
||||
IsTunEnabled = true,
|
||||
};
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.route.rules.Should().Contain(
|
||||
r => r.action == "reject"
|
||||
&& r.network != null && r.network.Contains("udp")
|
||||
&& r.port != null && r.port.Contains(5353),
|
||||
"the embedded tun rules must reject mDNS/NetBIOS noise");
|
||||
cfg.route.rules.Should().Contain(
|
||||
r => r.action == "reject"
|
||||
&& r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"),
|
||||
"the embedded tun rules must reject multicast traffic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses()
|
||||
{
|
||||
// Regression test: traffic addressed to the TUN interface's own addresses must
|
||||
// never reach an outbound. auto_route hijacks the default route, so `direct`
|
||||
// writes such a packet straight back into the TUN, which routes it to the
|
||||
// outbound again - an infinite loop that pins a CPU core. Observed in the wild
|
||||
// with WebRTC ICE connectivity checks against the TUN's own fc00::/7 ULA
|
||||
// address, sustaining ~8k packets/s out of the TUN interface.
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
config.TunModeItem.EnableTun = true;
|
||||
config.TunModeItem.EnableIPv6Address = true;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||
{
|
||||
IsTunEnabled = true,
|
||||
};
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
var tun = cfg.inbounds.First(i => i.type == "tun");
|
||||
tun.address.Should().NotBeNullOrEmpty();
|
||||
|
||||
foreach (var address in tun.address!)
|
||||
{
|
||||
var self = IPAddress.Parse(address.Split('/').First());
|
||||
var hostBits = self.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
|
||||
var expected = $"{self}/{hostBits}";
|
||||
cfg.route.rules.Should().Contain(
|
||||
r => r.action == "reject" && r.ip_cidr != null && r.ip_cidr.Contains(expected),
|
||||
$"traffic to the TUN's own address '{address}' must be rejected, not routed");
|
||||
}
|
||||
|
||||
// The match has to stay on the addresses themselves. sing-tun derives the TUN's DNS
|
||||
// entry from the address right after the interface's own, and every prefix offered
|
||||
// here leaves room for it, so a prefix match would drop system name lookups too.
|
||||
var dropRule = cfg.route.rules.First(r =>
|
||||
r.action == "reject" && r.method == "drop" && r.ip_cidr?.Count > 0);
|
||||
dropRule.ip_cidr!.Should().OnlyContain(c =>
|
||||
c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
|
||||
{
|
||||
|
||||
@@ -570,6 +570,51 @@ public class CoreConfigV2rayServiceTests
|
||||
directOutbound!.streamSettings.sockopt!.domainStrategy.Should().Be("UseIPv4");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address)
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||
|
||||
tunInbound.Should().NotBeNull();
|
||||
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0");
|
||||
tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0");
|
||||
|
||||
// EnableIPv6Address governs the interface address only, never the routing table.
|
||||
tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
|
||||
config.TunModeItem.EnableIPv6Address = false;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||
|
||||
tunInbound.Should().NotBeNull();
|
||||
tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':'));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_TunRouteExcludeAddress()
|
||||
{
|
||||
|
||||
@@ -134,4 +134,25 @@ public static class Extension
|
||||
.Replace("\r", replacement)
|
||||
.Replace("\n", replacement);
|
||||
}
|
||||
|
||||
public static async Task<TOutput> HandleSafe<TInput, TOutput>(
|
||||
this Interaction<TInput, TOutput> interaction,
|
||||
TInput input,
|
||||
TOutput defaultValue = default!)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await interaction.Handle(input);
|
||||
}
|
||||
catch (UnhandledInteractionException<TInput, TOutput> ex)
|
||||
{
|
||||
Logging.SaveLog($"Unhandled interaction exception for input: {input}", ex);
|
||||
return defaultValue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog($"Exception occurred while handling interaction for input: {input}", ex);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,12 +54,12 @@ internal static class WindowsUtils
|
||||
|
||||
public static async Task RemoveTunDevice()
|
||||
{
|
||||
var tunNameList = new List<string> { "singbox_tun", "xray_tun" };
|
||||
var tunNameList = new List<string> { "wintunsingbox_tun", "xray_tun" };
|
||||
foreach (var tunName in tunNameList)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sum = MD5.HashData(Encoding.UTF8.GetBytes($"wintun{tunName}"));
|
||||
var sum = MD5.HashData(Encoding.UTF8.GetBytes(tunName));
|
||||
var guid = new Guid(sum);
|
||||
var pnpUtilPath = @"C:\Windows\System32\pnputil.exe";
|
||||
var arg = $$""" /remove-device "SWD\Wintun\{{{guid}}}" """;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace ServiceLib.Handler.Fmt;
|
||||
|
||||
public class Hysteria2Fmt : BaseFmt
|
||||
@@ -169,7 +167,16 @@ public class Hysteria2Fmt : BaseFmt
|
||||
if (item.CertSha.IsNullOrEmpty())
|
||||
{
|
||||
item.CertSha = GetQueryDecoded(query, "pinSHA256");
|
||||
// NOTE:
|
||||
// To accommodate Xray changes,
|
||||
// some providers issue self-signed cert links with `insecure = false` and a certificate fingerprint,
|
||||
// breaking interoperability between Xray, official Hysteria 2 client, and sing-box.
|
||||
// Since this won't compromise the overall security model,
|
||||
// `insecure = true` is automatically set when a fingerprint is detected,
|
||||
// and the value is restored when generating configurations.
|
||||
item.AllowInsecure = Global.StringTrue;
|
||||
}
|
||||
item.EchConfigList = GetQueryDecoded(query, "ech");
|
||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||
{
|
||||
Ports = GetQueryDecoded(query, "mport"),
|
||||
@@ -212,6 +219,10 @@ public class Hysteria2Fmt : BaseFmt
|
||||
var sha = item.CertSha;
|
||||
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
||||
}
|
||||
if (!item.EchConfigList.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
|
||||
}
|
||||
var protocolExtraItem = item.GetProtocolExtra();
|
||||
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
|
||||
|
||||
@@ -1417,7 +1417,7 @@
|
||||
<value>新增 [Anytls] 節點</value>
|
||||
</data>
|
||||
<data name="TbRemoteDNS" xml:space="preserve">
|
||||
<value>遠程 DNS</value>
|
||||
<value>遠端 DNS</value>
|
||||
</data>
|
||||
<data name="TbDomesticDNS" xml:space="preserve">
|
||||
<value>直連 DNS</value>
|
||||
@@ -1429,16 +1429,16 @@
|
||||
<value>代理目標解析策略</value>
|
||||
</data>
|
||||
<data name="TbAddCommonDNSHosts" xml:space="preserve">
|
||||
<value>新增常用 DNS Hosts</value>
|
||||
<value>新增常用 DNS 主機對應</value>
|
||||
</data>
|
||||
<data name="TbFakeIP" xml:space="preserve">
|
||||
<value>FakeIP</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueries" xml:space="preserve">
|
||||
<value>阻止 SVCB 和 HTTPS 查詢</value>
|
||||
<value>封鎖 SVCB 與 HTTPS 查詢</value>
|
||||
</data>
|
||||
<data name="TbDNSHostsConfig" xml:space="preserve">
|
||||
<value>DNS Hosts:(“網域名稱1 ip1 ip2” 一行一個)</value>
|
||||
<value>DNS 主機對應:(每行一組「網域名稱1 ip1 ip2」)</value>
|
||||
</data>
|
||||
<data name="ThBasicDNSSettings" xml:space="preserve">
|
||||
<value>DNS 基礎設定</value>
|
||||
@@ -1447,49 +1447,49 @@
|
||||
<value>DNS 進階設定</value>
|
||||
</data>
|
||||
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
|
||||
<value>校驗相應地區域名 IP</value>
|
||||
<value>驗證區域網域名稱的 IP</value>
|
||||
</data>
|
||||
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
|
||||
<value>配置後,會對相應地區域名(如 geosite:cn - geoip:cn)的返回 IP 進行校驗,僅返回期望 IP</value>
|
||||
<value>設定後,系統會驗證區域網域名稱(例如 geosite:cn - geoip:cn)所傳回的 IP,並只傳回預期的 IP。</value>
|
||||
</data>
|
||||
<data name="TbCustomDNSEnable" xml:space="preserve">
|
||||
<value>啟用自訂 DNS</value>
|
||||
</data>
|
||||
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
|
||||
<value>自訂 DNS 已啟用,此頁面配置將無效</value>
|
||||
<value>已啟用自訂 DNS,此頁面的設定將不會生效</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>開啟後將阻止 ECH 和 HTTP/3 可用性查詢</value>
|
||||
<value>啟用後將封鎖 ECH 與 HTTP/3 可用性查詢</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>請填寫正確的配置範本</value>
|
||||
<value>請填寫正確的設定範本</value>
|
||||
</data>
|
||||
<data name="menuFullConfigTemplate" xml:space="preserve">
|
||||
<value>完整配置範本設定</value>
|
||||
<value>完整設定範本</value>
|
||||
</data>
|
||||
<data name="TbFullConfigTemplateEnable" xml:space="preserve">
|
||||
<value>啟用完整配置範本</value>
|
||||
<value>啟用完整設定範本</value>
|
||||
</data>
|
||||
<data name="TbRayFullConfigTemplate" xml:space="preserve">
|
||||
<value>v2ray 完整配置範本</value>
|
||||
<value>v2ray 完整設定範本</value>
|
||||
</data>
|
||||
<data name="TbRayFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>僅添加出站配置,routing.balancers 和 routing.rules.outboundTag,點擊查看文檔</value>
|
||||
<value>僅新增出站設定、routing.balancers 與 routing.rules.outboundTag。點選以查看說明文件</value>
|
||||
</data>
|
||||
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
|
||||
<value>不添加非代理協定出站</value>
|
||||
<value>不新增非代理協定出站</value>
|
||||
</data>
|
||||
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
|
||||
<value>設定上游代理 tag</value>
|
||||
<value>設定上游代理標籤</value>
|
||||
</data>
|
||||
<data name="TbSBFullConfigTemplate" xml:space="preserve">
|
||||
<value>sing-box 完整配置範本</value>
|
||||
<value>sing-box 完整設定範本</value>
|
||||
</data>
|
||||
<data name="TbSBFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>僅添加出站和端點配置,點擊查看文檔</value>
|
||||
<value>僅新增出站與端點設定。點選以查看說明文件</value>
|
||||
</data>
|
||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>此功能供高級用戶和有特殊需求的用戶使用。 啟用此功能後,將忽略 Core 的基礎設定,DNS 設定 ,路由設定。你需要保證系統代理的埠和流量統計等功能的配置正確,一切都由你來設定。</value>
|
||||
<value>此功能適合進階使用者與有特殊需求的使用者。啟用後,Core 基礎設定、DNS 設定與路由設定將被忽略。請確認系統代理連接埠、流量統計及其他相關設定皆正確;所有項目都必須由您自行設定。</value>
|
||||
</data>
|
||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>開始解析和處理訂閱內容</value>
|
||||
@@ -1498,10 +1498,10 @@
|
||||
<value>選擇節點</value>
|
||||
</data>
|
||||
<data name="TbFakeIPTips" xml:space="preserve">
|
||||
<value>默認全局生效,僅在 sing-box 中內置 FakeIP 過濾。</value>
|
||||
<value>預設會套用至全域;內建 FakeIP 過濾僅支援 sing-box。</value>
|
||||
</data>
|
||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>請至少添加一個節點</value>
|
||||
<value>請至少新增一個設定檔</value>
|
||||
</data>
|
||||
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
|
||||
<value>策略組</value>
|
||||
@@ -1627,7 +1627,7 @@
|
||||
<value>提供過期快取(Serve Stale)</value>
|
||||
</data>
|
||||
<data name="TbParallelQuery" xml:space="preserve">
|
||||
<value>并行查詢</value>
|
||||
<value>並行查詢</value>
|
||||
</data>
|
||||
<data name="TbDomesticDNSTips" xml:space="preserve">
|
||||
<value>預設僅在路由期間進行解析時調用</value>
|
||||
|
||||
@@ -48,12 +48,18 @@ public partial class CoreConfigSingboxService
|
||||
// packet straight back into the TUN, which hands it to the outbound again -
|
||||
// an infinite loop that pins a CPU core. Drop instead of rejecting so no
|
||||
// ICMP unreachable is generated back towards the same addresses.
|
||||
//
|
||||
// Match each address on its own, not the prefix it carries. On Linux sing-tun
|
||||
// registers Inet4Address[0].Addr().Next() with systemd-resolved as a "~." DNS
|
||||
// upstream, and every prefix offered here is a /30 or /126, so carrying the
|
||||
// prefix through would cover that resolver address too and drop every system
|
||||
// name lookup along with the loop.
|
||||
var tunAddresses = _coreConfig.inbounds.FirstOrDefault(i => i.type == "tun")?.address;
|
||||
if (tunAddresses?.Count > 0)
|
||||
{
|
||||
_coreConfig.route.rules.Add(new()
|
||||
{
|
||||
ip_cidr = [.. tunAddresses],
|
||||
ip_cidr = [.. tunAddresses.Select(ToSingleAddressPrefix)],
|
||||
action = "reject",
|
||||
method = "drop",
|
||||
});
|
||||
@@ -284,6 +290,14 @@ public partial class CoreConfigSingboxService
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToSingleAddressPrefix(string address)
|
||||
{
|
||||
var addr = address.Split('/').First();
|
||||
return IPAddress.TryParse(addr, out var ip)
|
||||
? $"{addr}/{(ip.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32)}"
|
||||
: address;
|
||||
}
|
||||
|
||||
private List<string> BuildRoutingDirectExe()
|
||||
{
|
||||
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -67,12 +67,14 @@ public partial class CoreConfigV2rayService
|
||||
|
||||
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
|
||||
tunInbound.settings.gateway = [address];
|
||||
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"];
|
||||
// Route both families into the tunnel regardless of EnableIPv6Address. That option only
|
||||
// controls whether the interface gets an IPv6 address; leaving ::/0 out of the routing
|
||||
// table makes IPv6 follow the system default route and bypass the tunnel entirely.
|
||||
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0", "::/0"];
|
||||
if (_config.TunModeItem.EnableIPv6Address == true)
|
||||
{
|
||||
var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
|
||||
tunInbound.settings.gateway.Add(address6);
|
||||
tunInbound.settings.autoSystemRoutingTable.Add("::/0");
|
||||
}
|
||||
|
||||
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
||||
@@ -81,7 +83,8 @@ public partial class CoreConfigV2rayService
|
||||
tunInbound.settings.autoOutboundsInterface = bindInterface;
|
||||
}
|
||||
tunInbound.sniffing = inbound.sniffing;
|
||||
tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
|
||||
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
|
||||
tunInbound.sniffing.routeOnly = true;
|
||||
|
||||
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
||||
{
|
||||
@@ -118,15 +121,8 @@ public partial class CoreConfigV2rayService
|
||||
includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList();
|
||||
includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList();
|
||||
|
||||
if (_config.TunModeItem.EnableIPv6Address)
|
||||
{
|
||||
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
|
||||
.Concat(includeListV6.Select(x => x.ToString())).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()).ToList();
|
||||
}
|
||||
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
|
||||
.Concat(includeListV6.Select(x => x.ToString())).ToList();
|
||||
}
|
||||
|
||||
_coreConfig.inbounds.Add(tunInbound);
|
||||
|
||||
@@ -512,6 +512,7 @@ public partial class CoreConfigV2rayService
|
||||
settings = new MaskSettings4Ray { value = kcpSeed },
|
||||
});
|
||||
}
|
||||
kcpFinalmask.udp?.Reverse();
|
||||
streamSettings.kcpSettings = kcpSettings;
|
||||
streamSettings.finalmask = kcpFinalmask;
|
||||
break;
|
||||
@@ -666,6 +667,7 @@ public partial class CoreConfigV2rayService
|
||||
version = 2,
|
||||
auth = _node.Password,
|
||||
};
|
||||
hy2Finalmask.udp?.Reverse();
|
||||
streamSettings.finalmask = hy2Finalmask;
|
||||
break;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public partial class AddServer2ViewModel : MyReactiveObject, ICloseable
|
||||
|
||||
BrowseServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
var fileName = await BrowseConfigFileInteraction.Handle(RxVoid.Default);
|
||||
var fileName = await BrowseConfigFileInteraction.HandleSafe(RxVoid.Default);
|
||||
if (fileName.IsNullOrEmpty())
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -85,8 +85,6 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
|
||||
#endregion Menu
|
||||
|
||||
private readonly SynchronizationContext _uiContext = SynchronizationContext.Current;
|
||||
|
||||
#region Init
|
||||
|
||||
public MainWindowViewModel()
|
||||
@@ -302,7 +300,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
.ObserveOn(RxSchedulers.MainThreadScheduler)
|
||||
.Subscribe(async blShow =>
|
||||
{
|
||||
await ShowHideWindowInteraction.Handle(blShow);
|
||||
await ShowHideWindowInteraction.HandleSafe(blShow);
|
||||
});
|
||||
|
||||
StatusBarViewModel.SetDefaultServerRequested
|
||||
@@ -413,14 +411,25 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
private async Task RefreshServersDispatcherAsync()
|
||||
{
|
||||
//await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
|
||||
_uiContext?.Post(_ => _ = RefreshServers(), null);
|
||||
await Signal.FromAsync(async () =>
|
||||
{
|
||||
await RefreshServers();
|
||||
return RxVoid.Default;
|
||||
})
|
||||
.SubscribeOn(RxSchedulers.MainThreadScheduler)
|
||||
.ToTask();
|
||||
}
|
||||
|
||||
private async Task RefreshSubscriptions()
|
||||
{
|
||||
//await Observable.Start(async () => await ProfilesViewModel.RefreshSubscriptions(), RxSchedulers.MainThreadScheduler);
|
||||
|
||||
_uiContext?.Post(_ => _ = ProfilesViewModel.RefreshSubscriptions(), null);
|
||||
await Signal.FromAsync(async () =>
|
||||
{
|
||||
await ProfilesViewModel.RefreshSubscriptions();
|
||||
return RxVoid.Default;
|
||||
})
|
||||
.SubscribeOn(RxSchedulers.MainThreadScheduler)
|
||||
.ToTask();
|
||||
}
|
||||
|
||||
#endregion Servers && Groups
|
||||
@@ -467,7 +476,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
var stringData = clipboardData;
|
||||
if (clipboardData == null)
|
||||
{
|
||||
var result = await ReadTextFromClipboardInteraction.Handle(RxVoid.Default);
|
||||
var result = await ReadTextFromClipboardInteraction.HandleSafe(RxVoid.Default);
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
|
||||
@@ -490,7 +499,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
|
||||
public async Task AddServerViaScanAsync()
|
||||
{
|
||||
var result = await ScanScreenInteraction.Handle(RxVoid.Default);
|
||||
var result = await ScanScreenInteraction.HandleSafe(RxVoid.Default);
|
||||
await ScanScreenResult(result);
|
||||
}
|
||||
|
||||
@@ -502,7 +511,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
|
||||
public async Task AddServerViaImageAsync()
|
||||
{
|
||||
var imageFileName = await BrowseImageFileInteraction.Handle(RxVoid.Default);
|
||||
var imageFileName = await BrowseImageFileInteraction.HandleSafe(RxVoid.Default);
|
||||
await AddScanResultAsync(imageFileName);
|
||||
}
|
||||
|
||||
@@ -691,10 +700,12 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
//{
|
||||
// await ClashProxiesViewModel.ProxiesReload();
|
||||
//}, RxSchedulers.MainThreadScheduler);
|
||||
RxSchedulers.MainThreadScheduler.Schedule(async () =>
|
||||
{
|
||||
await ClashProxiesViewModel.ProxiesReload();
|
||||
});
|
||||
await Signal.FromAsync(async () =>
|
||||
{
|
||||
await ClashProxiesViewModel.ProxiesReload();
|
||||
return RxVoid.Default;
|
||||
}).SubscribeOn(RxSchedulers.MainThreadScheduler)
|
||||
.ToTask();
|
||||
}
|
||||
|
||||
ReloadResult(showClashUI);
|
||||
|
||||
@@ -74,7 +74,7 @@ public partial class MsgViewModel : MyReactiveObject
|
||||
{
|
||||
try
|
||||
{
|
||||
await DispatcherShowMsgInteraction.Handle(sb.ToString());
|
||||
await DispatcherShowMsgInteraction.HandleSafe(sb.ToString());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -138,13 +138,7 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
|
||||
|
||||
await RefreshServers();
|
||||
|
||||
try
|
||||
{
|
||||
await ProfilesFocusInteraction.Handle(RxVoid.Default);
|
||||
}
|
||||
catch (UnhandledInteractionException<RxVoid, RxVoid>)
|
||||
{
|
||||
}
|
||||
await ProfilesFocusInteraction.HandleSafe(RxVoid.Default);
|
||||
}
|
||||
|
||||
private async Task ServerFilterChanged(bool c)
|
||||
|
||||
@@ -345,13 +345,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
|
||||
await RefreshServers();
|
||||
|
||||
try
|
||||
{
|
||||
await ProfilesFocusInteraction.Handle(RxVoid.Default);
|
||||
}
|
||||
catch (UnhandledInteractionException<RxVoid, RxVoid>)
|
||||
{
|
||||
}
|
||||
await ProfilesFocusInteraction.HandleSafe(RxVoid.Default);
|
||||
}
|
||||
|
||||
private async Task ServerFilterChanged(bool c)
|
||||
@@ -395,13 +389,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
SelectedProfile = selected ?? lstModel.First();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await DispatcherRefreshServersBizInteraction.Handle(RxVoid.Default);
|
||||
}
|
||||
catch (UnhandledInteractionException<RxVoid, RxVoid>)
|
||||
{
|
||||
}
|
||||
await DispatcherRefreshServersBizInteraction.HandleSafe(RxVoid.Default);
|
||||
}
|
||||
|
||||
public async Task RefreshSubscriptions()
|
||||
@@ -419,7 +407,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
|
||||
public async Task AdjustMainLvColWidth()
|
||||
{
|
||||
await AdjustMainLvColWidthInteraction.Handle(RxVoid.Default);
|
||||
await AdjustMainLvColWidthInteraction.HandleSafe(RxVoid.Default);
|
||||
}
|
||||
|
||||
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
|
||||
@@ -535,7 +523,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
||||
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -556,7 +544,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
|
||||
private async Task RemoveDuplicateServer()
|
||||
{
|
||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
||||
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -631,7 +619,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
return;
|
||||
}
|
||||
|
||||
await ShareServerInteraction.Handle(url);
|
||||
await ShareServerInteraction.HandleSafe(url);
|
||||
}
|
||||
|
||||
private async Task GenGroupAllServer()
|
||||
@@ -799,13 +787,13 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
}
|
||||
else
|
||||
{
|
||||
await SetClipboardDataInteraction.Handle((string)result.Data);
|
||||
await SetClipboardDataInteraction.HandleSafe((string)result.Data);
|
||||
NoticeManager.Instance.SendMessage(ResUI.OperationSuccess);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await SaveFileDialogInteraction.Handle(item);
|
||||
await SaveFileDialogInteraction.HandleSafe(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,11 +842,11 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
{
|
||||
if (blEncode)
|
||||
{
|
||||
await SetClipboardDataInteraction.Handle(Utils.Base64Encode(sb.ToString()));
|
||||
await SetClipboardDataInteraction.HandleSafe(Utils.Base64Encode(sb.ToString()));
|
||||
}
|
||||
else
|
||||
{
|
||||
await SetClipboardDataInteraction.Handle(sb.ToString());
|
||||
await SetClipboardDataInteraction.HandleSafe(sb.ToString());
|
||||
}
|
||||
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
|
||||
}
|
||||
@@ -881,7 +869,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
|
||||
if (!result.IsNullOrEmpty())
|
||||
{
|
||||
await SetClipboardDataInteraction.Handle(result);
|
||||
await SetClipboardDataInteraction.HandleSafe(result);
|
||||
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
|
||||
}
|
||||
else
|
||||
@@ -925,7 +913,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
return;
|
||||
}
|
||||
|
||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
||||
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,21 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
|
||||
[Reactive]
|
||||
public partial bool AutoSort { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string OutboundTag { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string Remarks { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string Port { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string Network { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial bool Enabled { get; set; }
|
||||
|
||||
public ReactiveCommand<RxVoid, RxVoid> SelectProfileCmd { get; }
|
||||
public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
|
||||
|
||||
@@ -57,6 +72,11 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
|
||||
IP = Utils.List2String(SelectedSource.Ip, true);
|
||||
Process = Utils.List2String(SelectedSource.Process, true);
|
||||
RuleType = SelectedSource.RuleType?.ToString();
|
||||
OutboundTag = SelectedSource.OutboundTag;
|
||||
Remarks = SelectedSource.Remarks;
|
||||
Port = SelectedSource.Port;
|
||||
Network = SelectedSource.Network;
|
||||
Enabled = SelectedSource.Enabled;
|
||||
}
|
||||
|
||||
private async Task SaveRulesAsync()
|
||||
@@ -80,6 +100,11 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
|
||||
SelectedSource.Protocol = ProtocolItems?.ToList();
|
||||
SelectedSource.InboundTag = InboundTagItems?.ToList();
|
||||
SelectedSource.RuleType = RuleType.IsNullOrEmpty() ? null : Enum.Parse<ERuleType>(RuleType);
|
||||
SelectedSource.OutboundTag = OutboundTag;
|
||||
SelectedSource.Remarks = Remarks;
|
||||
SelectedSource.Port = Port;
|
||||
SelectedSource.Network = Network;
|
||||
SelectedSource.Enabled = Enabled;
|
||||
|
||||
var hasRule = SelectedSource.Domain?.Count > 0
|
||||
|| SelectedSource.Ip?.Count > 0
|
||||
@@ -110,8 +135,7 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
|
||||
var profileItem = await profileSelectViewModel.GetProfileItem();
|
||||
if (profileItem != null)
|
||||
{
|
||||
SelectedSource.OutboundTag = profileItem.Remarks;
|
||||
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
|
||||
OutboundTag = profileItem.Remarks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
||||
});
|
||||
ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
var fileName = await BrowseRulesFileInteraction.Handle(RxVoid.Default);
|
||||
var fileName = await BrowseRulesFileInteraction.HandleSafe(RxVoid.Default);
|
||||
await ImportRulesFromFileAsync(fileName);
|
||||
});
|
||||
ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
@@ -156,7 +156,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
||||
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
|
||||
return;
|
||||
}
|
||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
||||
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -199,7 +199,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
await SetClipboardDataInteraction.Handle(JsonUtils.Serialize(lst, options));
|
||||
await SetClipboardDataInteraction.HandleSafe(JsonUtils.Serialize(lst, options));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
||||
var stringData = clipboardData;
|
||||
if (clipboardData == null)
|
||||
{
|
||||
var result = await ReadTextFromClipboardInteraction.Handle(RxVoid.Default);
|
||||
var result = await ReadTextFromClipboardInteraction.HandleSafe(RxVoid.Default);
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
|
||||
@@ -315,7 +315,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
||||
private async Task<int> AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData)
|
||||
{
|
||||
var blReplace = false;
|
||||
if (await ShowYesNoInteraction.Handle(ResUI.AddBatchRoutingRulesYesNo) == false)
|
||||
if (await ShowYesNoInteraction.HandleSafe(ResUI.AddBatchRoutingRulesYesNo) == false)
|
||||
{
|
||||
blReplace = true;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ public partial class RoutingSettingViewModel : MyReactiveObject
|
||||
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
|
||||
return;
|
||||
}
|
||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
||||
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ public partial class StatusBarViewModel : MyReactiveObject
|
||||
sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}");
|
||||
sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}");
|
||||
|
||||
await SetClipboardDataInteraction.Handle(sb.ToString());
|
||||
await SetClipboardDataInteraction.HandleSafe(sb.ToString());
|
||||
}
|
||||
|
||||
private async Task AddServerViaClipboard()
|
||||
@@ -297,21 +297,14 @@ public partial class StatusBarViewModel : MyReactiveObject
|
||||
return;
|
||||
}
|
||||
|
||||
var models = new List<ComboItem>();
|
||||
BlServers = true;
|
||||
foreach (var it in lstModel)
|
||||
{
|
||||
var name = it.GetSummary();
|
||||
var models = lstModel.Select(it => new ComboItem { ID = it.IndexId, Text = it.GetSummary() }).ToList();
|
||||
|
||||
var item = new ComboItem() { ID = it.IndexId, Text = name };
|
||||
models.Add(item);
|
||||
if (_config.IndexId == it.IndexId)
|
||||
{
|
||||
SelectedServer = item;
|
||||
}
|
||||
}
|
||||
BlServers = true;
|
||||
Servers.Clear();
|
||||
Servers.AddRange(models);
|
||||
|
||||
// Update the ItemsSource before SelectedItem so a collection reset does not clear the tray selection.
|
||||
SelectedServer = models.FirstOrDefault(it => it.ID == _config.IndexId) ?? new();
|
||||
}
|
||||
|
||||
private void ServerSelectedChanged(bool c)
|
||||
@@ -389,14 +382,7 @@ public partial class StatusBarViewModel : MyReactiveObject
|
||||
|
||||
if (blChange)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DispatcherRefreshIconInteraction.Handle(RxVoid.Default);
|
||||
}
|
||||
catch (UnhandledInteractionException<RxVoid, RxVoid>)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
await DispatcherRefreshIconInteraction.HandleSafe(RxVoid.Default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +418,7 @@ public partial class StatusBarViewModel : MyReactiveObject
|
||||
{
|
||||
NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
|
||||
ReloadRequested.Publish();
|
||||
await DispatcherRefreshIconInteraction.Handle(RxVoid.Default);
|
||||
await DispatcherRefreshIconInteraction.HandleSafe(RxVoid.Default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +455,7 @@ public partial class StatusBarViewModel : MyReactiveObject
|
||||
}
|
||||
else
|
||||
{
|
||||
var password = await PasswordInputInteraction.Handle(RxVoid.Default);
|
||||
var password = await PasswordInputInteraction.HandleSafe(RxVoid.Default);
|
||||
if (password.IsNullOrEmpty())
|
||||
{
|
||||
_config.TunModeItem.EnableTun = false;
|
||||
|
||||
@@ -9,6 +9,11 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
||||
|
||||
[Reactive]
|
||||
public partial string CustomCoreType { get; set; }
|
||||
[Reactive]
|
||||
public partial string PrevProfile { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string NextProfile { get; set; }
|
||||
|
||||
public ReactiveCommand<RxVoid, RxVoid> SelectPrevProfileCmd { get; }
|
||||
public ReactiveCommand<RxVoid, RxVoid> SelectNextProfileCmd { get; }
|
||||
@@ -23,8 +28,7 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
||||
var profileItem = await SelectProfileAsync();
|
||||
if (profileItem != null)
|
||||
{
|
||||
SelectedSource?.PrevProfile = profileItem.Remarks;
|
||||
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
|
||||
PrevProfile = profileItem.Remarks;
|
||||
}
|
||||
});
|
||||
SelectNextProfileCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
@@ -32,8 +36,7 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
||||
var profileItem = await SelectProfileAsync();
|
||||
if (profileItem != null)
|
||||
{
|
||||
SelectedSource?.NextProfile = profileItem.Remarks;
|
||||
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
|
||||
NextProfile = profileItem.Remarks;
|
||||
}
|
||||
});
|
||||
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
@@ -43,6 +46,8 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
||||
|
||||
SelectedSource = subItem.Id.IsNullOrEmpty() ? subItem : JsonUtils.DeepCopy(subItem);
|
||||
CustomCoreType = SelectedSource.CustomCoreType?.ToString() ?? string.Empty;
|
||||
PrevProfile = SelectedSource.PrevProfile;
|
||||
NextProfile = SelectedSource.NextProfile;
|
||||
}
|
||||
|
||||
private async Task SaveSubAsync()
|
||||
@@ -72,6 +77,8 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
||||
}
|
||||
|
||||
SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null;
|
||||
SelectedSource.PrevProfile = PrevProfile;
|
||||
SelectedSource.NextProfile = NextProfile;
|
||||
|
||||
if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0)
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ public partial class SubSettingViewModel : MyReactiveObject
|
||||
}, canEditRemove);
|
||||
SubShareCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
await ShareSubInteraction.Handle(SelectedSource?.Url);
|
||||
await ShareSubInteraction.HandleSafe(SelectedSource?.Url);
|
||||
}, canEditRemove);
|
||||
|
||||
_ = Init();
|
||||
@@ -84,7 +84,7 @@ public partial class SubSettingViewModel : MyReactiveObject
|
||||
|
||||
private async Task DeleteSubAsync()
|
||||
{
|
||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
||||
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
|
||||
.Subscribe(InitializeData)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnabled.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Port, v => v.txtPort.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Enabled, v => v.togEnabled.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Domain, v => v.txtDomain.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.IP, v => v.txtIP.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Process, v => v.txtProcess.Text).DisposeWith(disposables);
|
||||
|
||||
@@ -25,8 +25,8 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
||||
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.ConvertTarget, v => v.cmbConvertTarget.SelectedValue).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.PrevProfile, v => v.txtPrevProfile.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.NextProfile, v => v.txtNextProfile.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.PrevProfile, v => v.txtPrevProfile.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.NextProfile, v => v.txtNextProfile.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.SelectedValue).DisposeWith(disposables);
|
||||
|
||||
@@ -23,11 +23,11 @@ public partial class RoutingRuleDetailsWindow
|
||||
.Subscribe(InitializeData)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnabled.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Port, v => v.txtPort.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Network, v => v.cmbNetwork.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Enabled, v => v.togEnabled.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Domain, v => v.txtDomain.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.IP, v => v.txtIP.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Process, v => v.txtProcess.Text).DisposeWith(disposables);
|
||||
|
||||
@@ -22,8 +22,8 @@ public partial class SubEditWindow
|
||||
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.ConvertTarget, v => v.cmbConvertTarget.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.PrevProfile, v => v.txtPrevProfile.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.NextProfile, v => v.txtNextProfile.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.PrevProfile, v => v.txtPrevProfile.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.NextProfile, v => v.txtNextProfile.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.Text).DisposeWith(disposables);
|
||||
|
||||
Reference in New Issue
Block a user