Compare commits

...

27 Commits

Author SHA1 Message Date
DHR60 09ea4890a7 Fix (#9771) 2026-07-16 17:37:17 +08:00
DHR60 5cc2aaba13 Fix (#9769) 2026-07-16 17:37:04 +08:00
DHR60 b8889bad86 Fix (#9768) 2026-07-16 17:36:51 +08:00
JieXu 5dd5b25869 Update Directory.Packages.props (#9766) 2026-07-16 11:48:57 +08:00
2dust fafdb4a0b6 up 7.24.0 2026-07-16 11:42:28 +08:00
2dust 223642dd67 Fix
https://github.com/2dust/v2rayN/issues/9765
2026-07-16 11:39:47 +08:00
Miheichev Aleksandr Sergeevich 165e0bf9e9 chore(deps): update NuGet dependencies (#9745)
- Downloader 5.9.0 -> 5.9.4
- NLog 6.1.3 -> 6.1.4
- Repobot.SQLite.Unofficial 3.53.3 -> 3.53.3.8
- sqlite-net-e 1.11.0 -> 1.11.285
- YamlDotNet 18.0.0 -> 18.1.0

sqlite-net-e and Repobot.SQLite.Unofficial are bumped together: the newer
sqlite-net-e moves to SQLitePCLRaw.config.e_sqlite3 3.0.3.

SkiaSharp.NativeAssets.Linux intentionally stays at 3.119.4 to match the
managed SkiaSharp resolved through Avalonia.Skia 12.1.0; bumping it alone
to 4.x would compile but break rendering at runtime on Linux. It should
follow Avalonia when Avalonia itself moves to SkiaSharp 4.
ReactiveUI.Avalonia stays at 12.0.3: the numerically higher 14.x line
targets Avalonia 11 / ReactiveUI 19 and would be an effective downgrade
for the Avalonia 12 stack.
2026-07-15 19:12:56 +08:00
DHR60 e615314582 Xray tun route only (#9759) 2026-07-15 19:12:34 +08:00
DHR60 ca9978b625 Fix (#9758) 2026-07-15 18:12:55 +08:00
DHR60 74ab7ad097 Add ProtectCoreTypeList (#9756)
* Add ProtectCoreTypeList

* Add process path restrictions
2026-07-15 17:32:07 +08:00
DHR60 8f5cbad988 Xray precore for custom config (#9754)
* Xray precore for custom config

* Fix

* Custom Pre

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-15 17:23:21 +08:00
2dust 1ab9757498 Revert "fix: 在窗口初始化前设置保存的尺寸,消除启动时偏右下偏移 (#9498)"
This reverts commit 8147b39723.
https://github.com/2dust/v2rayN/issues/9740
2026-07-14 17:27:30 +08:00
DHR60 50ab02e8a9 Desktop DragDropSort (#9734) 2026-07-14 09:57:00 +08:00
2dust b9a1c129c4 Fixed the issue with RefreshIcon's call in the desktop version. 2026-07-14 09:52:01 +08:00
tt2563 996eb51f9b Update Traditional Chinese translation (#9742) 2026-07-14 09:11:48 +08:00
Miheichev Aleksandr Sergeevich b9a58e7137 i18n(ru): translate newly added UI strings (#9733)
Add Russian translations for the 20 resource strings introduced up to
v7.23.4 that were missing from ResUI.ru.resx:

- allowInsecure deprecation warning and insecure-configuration message
- Xray HTTP outbound custom headers (label, tip, validation message)
- Hysteria2 Realm URL (label, format tip, validation message)
- TUN route exclude address (label, tip, invalid-address message)
- Final Fragment option and Root Certificate Provider (labels and tips)
- DNS via Bridge, Gecko Packet Size, Legacy Protect tip,
  Verify Peer Cert By Name, options-conflict message

Translated from the zh-Hans source and cross-checked against the English
resource. Existing Russian terminology conventions are preserved, and key
ordering mirrors the English resource file.
2026-07-14 09:11:14 +08:00
DHR60 9664ee380e ViewModel-First (#9621)
* Interaction

* Remove view action

* ViewModel-first

* MainGirdOrientation Hot Reload

* Fix avalonia preview

* Remove CloseWindowInteraction

* Fix

* Avoid threading issues

* Fix

* ProfilesSelect

* Try fix previewer

* Remove AppEvents.ProfilesRefreshRequested

* Remove AppEvents.SubscriptionsRefreshRequested

* Remove AppEvents.ProxiesReloadRequested

* Remove AppEvents.AdjustMainLvColWidthRequested

* Remove AppEvents.SetDefaultServerRequested

* Remove AppEvents.RoutingsMenuRefreshRequested

* Remove AppEvents.TestServerRequested

* Remove AppEvents.InboundDisplayRequested

* Remove AppEvents.SubscriptionsUpdateRequested

* Remove AppEvents.ShowHideWindowRequested

* Remove AppEvents.ReloadRequested

* Remove AppEvents.AddServerRequested

* Fix
2026-07-14 09:05:05 +08:00
DHR60 6f50206606 Update Avalonia to 12.1 (#9148) 2026-07-13 09:41:28 +08:00
2dust 1de83f96ed up 7.23.4 2026-07-11 17:44:19 +08:00
2dust 994bc1ca6a Code clean 2026-07-11 17:43:32 +08:00
2dust 3ca49dd9db Add option to hide IP info column 2026-07-11 16:05:31 +08:00
DHR60 a0bd8f9934 Remove fallbackTag (#9728) 2026-07-11 15:18:16 +08:00
006lp 88d59488fd feat: support custom headers for Xray HTTP outbounds (#9690)
* feat: support custom headers for Xray HTTP outbounds

* feat: simplify HTTP outbound headers input

* feat: validate JSON syntax in HTTP headers

  Keep HTTP outbound headers stored as raw text, while validating
  header names and values against RFC 9110/9112 syntax. Support string
  arrays for multi-value headers, reject duplicate JSON header keys, and
  clarify the invalid header JSON message.

* feat: refactor HTTP header parsing into utility

* Simplify code

* Simplify code

* Update CoreConfigV2rayServiceTests.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-11 14:31:17 +08:00
DHR60 2c70b018ce Revert "Fix" (#9727)
* Revert "Fix"

This reverts commit 45ab7503e3.

* Fix
2026-07-11 14:02:12 +08:00
Kurobac b6ab428dfc Fix Linux tray tooltip identity (#9709)
Keep the tray tooltip text fixed to the application name on Linux so desktop environments do not treat server changes as tray identity changes. Non-Linux platforms continue to show the current server information.
2026-07-11 11:42:00 +08:00
2dust 45ab7503e3 Fix
https://github.com/2dust/v2rayN/issues/9699
2026-07-11 11:20:36 +08:00
2dust 1768b4a7ee Update winget-publish.yml 2026-07-09 20:51:04 +08:00
148 changed files with 2686 additions and 2415 deletions
+5
View File
@@ -83,6 +83,11 @@ jobs:
working-directory: ./v2rayN working-directory: ./v2rayN
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
- name: Remove Debug Symbols
shell: bash
run: |
find ${{ matrix.arch }} -type f -name '*.pdb' -delete
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v7.0.1 uses: actions/upload-artifact@v7.0.1
with: with:
+7 -2
View File
@@ -23,8 +23,13 @@ jobs:
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1 $targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
$x64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-64\.zip' | Select -ExpandProperty browser_download_url $assets = $targetRelease | Select -ExpandProperty assets -First 1
$arm64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-arm64\.zip' | Select -ExpandProperty browser_download_url $x64InstallerUrl = $assets | Where-Object { $_.name -eq 'v2rayN-windows-64.zip' } | Select -ExpandProperty browser_download_url
$arm64InstallerUrl = $assets | Where-Object { $_.name -eq 'v2rayN-windows-arm64.zip' } | Select -ExpandProperty browser_download_url
if (-not $x64InstallerUrl -or -not $arm64InstallerUrl) {
throw "Could not find required installers in release assets."
}
$ver = $targetRelease.tag_name $ver = $targetRelease.tag_name
+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.23.3</Version> <Version>7.24.0</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
+13 -13
View File
@@ -5,16 +5,16 @@
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled> <CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" /> <PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.18" /> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" /> <PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.12.2" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" /> <PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="12.0.3" />
<PackageVersion Include="CliWrap" Version="3.10.2" /> <PackageVersion Include="CliWrap" Version="3.10.2" />
<PackageVersion Include="Downloader" Version="5.9.0" /> <PackageVersion Include="Downloader" Version="5.9.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
@@ -22,17 +22,17 @@
<PackageVersion Include="ReactiveUI" Version="23.2.28" /> <PackageVersion Include="ReactiveUI" Version="23.2.28" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" /> <PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" /> <PackageVersion Include="Semi.Avalonia" Version="12.1.0" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0" />
<PackageVersion Include="NLog" Version="6.1.3" /> <PackageVersion Include="NLog" Version="6.1.4" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" /> <PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3" /> <PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" /> <PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" /> <PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="18.0.0" /> <PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" /> <PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup> </ItemGroup>
@@ -130,6 +130,25 @@ internal static class CoreConfigTestFactory
}; };
} }
public static ProfileItem CreateHttpNode(ECoreType coreType, string indexId = "node-http-1",
string remarks = "demo-http")
{
return new ProfileItem
{
IndexId = indexId,
ConfigType = EConfigType.HTTP,
CoreType = coreType,
Remarks = remarks,
Address = "proxy.example.com",
Port = 8080,
Password = "pass",
Username = "user",
Network = nameof(ETransport.raw),
StreamSecurity = string.Empty,
Subid = string.Empty,
};
}
public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks, public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks,
IEnumerable<string> childIndexIds) IEnumerable<string> childIndexIds)
{ {
@@ -28,6 +28,39 @@ public class CoreConfigV2rayServiceTests
v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed)); v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
} }
[Fact]
public void GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateHttpNode(ECoreType.Xray);
node.SetProtocolExtra(node.GetProtocolExtra() with
{
HttpHeaders = "{\"User-Agent\":\"v2rayN\",\"Set-Cookie\":[\"a=1\",\"b=2\"]}",
});
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 outbound = cfg.outbounds.First(o => o.tag == Global.ProxyTag && o.protocol == "http");
outbound.settings.address?.ToString().Should().Be("proxy.example.com");
outbound.settings.port.Should().Be(8080);
outbound.settings.user.Should().Be("user");
outbound.settings.pass.Should().Be("pass");
outbound.settings.level.Should().Be(1);
outbound.settings.headers.Should().NotBeNull();
var headers = JsonUtils.ParseJson(outbound.settings.headers.ToString());
headers["User-Agent"]!.GetValue<string>().Should().Be("v2rayN");
headers["Set-Cookie"]!.AsArray()
.Select(item => item!.GetValue<string>())
.Should().Equal("a=1", "b=2");
outbound.settings.servers.Should().BeNull();
outbound.settings.vnext.Should().BeNull();
}
[Fact] [Fact]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer() public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
{ {
+6
View File
@@ -0,0 +1,6 @@
namespace ServiceLib.Base;
public interface ICloseable
{
public event EventHandler? RequestClose;
}
+7
View File
@@ -0,0 +1,7 @@
namespace ServiceLib.Base;
public interface IWindowDialog
{
public Task<bool> ShowDialogAsync<TViewModel>(TViewModel vm)
where TViewModel : class;
}
@@ -3,5 +3,4 @@ namespace ServiceLib.Base;
public class MyReactiveObject : ReactiveObject public class MyReactiveObject : ReactiveObject
{ {
protected static Config? _config; protected static Config? _config;
protected Func<EViewAction, object?, Task<bool>>? _updateView;
} }
-36
View File
@@ -1,36 +0,0 @@
namespace ServiceLib.Enums;
public enum EViewAction
{
CloseWindow,
ShowYesNo,
SaveFileDialog,
AddBatchRoutingRulesYesNo,
SetClipboardData,
AddServerViaClipboard,
ImportRulesFromClipboard,
ProfilesFocus,
ShareSub,
ShareServer,
ScanScreenTask,
ScanImageTask,
BrowseServer,
ImportRulesFromFile,
InitSettingFont,
PasswordInput,
SubEditWindow,
RoutingRuleSettingWindow,
RoutingRuleDetailsWindow,
AddServerWindow,
AddServer2Window,
AddGroupServerWindow,
DNSSettingWindow,
RoutingSettingWindow,
OptionSettingWindow,
FullConfigTemplateWindow,
GlobalHotkeySettingWindow,
SubSettingWindow,
DispatcherRefreshServersBiz,
DispatcherRefreshIcon,
DispatcherShowMsg,
}
-14
View File
@@ -2,16 +2,9 @@ namespace ServiceLib.Events;
public static class AppEvents public static class AppEvents
{ {
public static readonly EventChannel<Unit> ReloadRequested = new();
public static readonly EventChannel<bool?> ShowHideWindowRequested = new();
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new(); public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
public static readonly EventChannel<bool> HasUpdateNotified = new(); public static readonly EventChannel<bool> HasUpdateNotified = new();
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
public static readonly EventChannel<Unit> ProxiesReloadRequested = new();
public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new(); public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new();
public static readonly EventChannel<string> SendSnackMsgRequested = new(); public static readonly EventChannel<string> SendSnackMsgRequested = new();
@@ -20,12 +13,5 @@ public static class AppEvents
public static readonly EventChannel<Unit> AppExitRequested = new(); public static readonly EventChannel<Unit> AppExitRequested = new();
public static readonly EventChannel<bool> ShutdownRequested = new(); public static readonly EventChannel<bool> ShutdownRequested = new();
public static readonly EventChannel<Unit> AdjustMainLvColWidthRequested = new();
public static readonly EventChannel<string> SetDefaultServerRequested = new();
public static readonly EventChannel<Unit> RoutingsMenuRefreshRequested = new();
public static readonly EventChannel<Unit> TestServerRequested = new();
public static readonly EventChannel<Unit> InboundDisplayRequested = new();
public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new(); public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();
} }
@@ -193,12 +193,16 @@ public class CoreConfigContextBuilder
if (preSocksItem != null) if (preSocksItem != null)
{ {
var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem); var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem);
var protectCoreTypeList = new HashSet<ECoreType>(nodeContext.ProtectCoreTypeList) { nodeContext.RunCoreType };
return preSocksResult with return preSocksResult with
{ {
Context = preSocksResult.Context with Context = preSocksResult.Context with
{ {
ProtectDomainList = ProtectDomainList =
[.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []], [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
ProtectCoreTypeList = protectCoreTypeList,
}, },
}; };
} }
+3 -2
View File
@@ -164,7 +164,7 @@ public static class ConfigHandler
config.ClashUIItem.ConnectionsColumnItem ??= []; config.ClashUIItem.ConnectionsColumnItem ??= [];
config.SystemProxyItem ??= new(); config.SystemProxyItem ??= new();
config.WebDavItem ??= new(); config.WebDavItem ??= new();
config.CheckUpdateItem ??= new(); config.CheckUpdateItem ??= new();
config.Fragment4RayItem ??= new() config.Fragment4RayItem ??= new()
{ {
Packets = "tlshello", Packets = "tlshello",
@@ -1511,7 +1511,8 @@ public static class ConfigHandler
else if (node.ConfigType == EConfigType.Custom else if (node.ConfigType == EConfigType.Custom
&& node.PreSocksPort is > 0 and <= 65535) && node.PreSocksPort is > 0 and <= 65535)
{ {
var preCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; var customPreCoreType = AppManager.Instance.GetCoreType(null, EConfigType.Custom);
var preCoreType = (enableLegacyProtect && config.TunModeItem.EnableTun) ? ECoreType.sing_box : customPreCoreType;
itemSocks = new ProfileItem() itemSocks = new ProfileItem()
{ {
CoreType = preCoreType, CoreType = preCoreType,
+2 -1
View File
@@ -10,6 +10,7 @@ public sealed class AppManager
private int? _statePort2; private int? _statePort2;
public static AppManager Instance => _instance.Value; public static AppManager Instance => _instance.Value;
public Config Config => _config; public Config Config => _config;
public IWindowDialog WindowDialog { get; set; } = null!;
public int StatePort public int StatePort
{ {
@@ -662,7 +663,7 @@ public sealed class AppManager
return Global.SsSecuritiesInSingbox; return Global.SsSecuritiesInSingbox;
} }
public ECoreType GetCoreType(ProfileItem profileItem, EConfigType eConfigType) public ECoreType GetCoreType(ProfileItem? profileItem, EConfigType eConfigType)
{ {
if (profileItem?.CoreType != null) if (profileItem?.CoreType != null)
{ {
@@ -288,6 +288,7 @@ public class CertPemManager
collection.ImportFromPem(pemText); collection.ImportFromPem(pemText);
return collection; return collection;
}); });
private static readonly Lazy<X509Certificate2Collection> _mozillaRootCerts = new(() => private static readonly Lazy<X509Certificate2Collection> _mozillaRootCerts = new(() =>
{ {
var pemText = EmbedUtils.GetEmbedText(Global.MozillaRootCertFileName); var pemText = EmbedUtils.GetEmbedText(Global.MozillaRootCertFileName);
@@ -295,6 +296,7 @@ public class CertPemManager
collection.ImportFromPem(pemText); collection.ImportFromPem(pemText);
return collection; return collection;
}); });
private X509Certificate2Collection BuildTrustedCertificateCollection() private X509Certificate2Collection BuildTrustedCertificateCollection()
{ {
if (_config.GuiItem.RootCertProvider == Global.ChromeRootProvider) if (_config.GuiItem.RootCertProvider == Global.ChromeRootProvider)
+12 -1
View File
@@ -34,7 +34,18 @@ public class CoreAdminManager
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine("#!/bin/bash"); sb.AppendLine("#!/bin/bash");
var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}"; var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}";
sb.AppendLine($"exec sudo -S -- {cmdLine}");
// Passing environment variables to the sudo command, here it only xray or sing-box.
if (coreInfo.Environment.Count > 0)
{
var envArgs = string.Join(" ", coreInfo.Environment.Where(kv => kv.Value.IsNotEmpty()).Select(kv => $"{kv.Key}={kv.Value.AppendQuotes()}"));
sb.AppendLine($"exec sudo -S -- env {envArgs} {cmdLine}");
}
else
{
sb.AppendLine($"exec sudo -S -- {cmdLine}");
}
var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true); var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
var procService = new ProcessService( var procService = new ProcessService(
+2
View File
@@ -8,8 +8,10 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new()); private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value; public static CoreManager Instance => _instance.Value;
private Config _config; private Config _config;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private WindowsJobService? _processJob; private WindowsJobService? _processJob;
private ProcessService? _processService; private ProcessService? _processService;
private ProcessService? _processPreService; private ProcessService? _processPreService;
private bool _linuxSudo = false; private bool _linuxSudo = false;
@@ -103,6 +103,7 @@ public class UIItem
public bool MacOSShowInDock { get; set; } public bool MacOSShowInDock { get; set; }
public List<ColumnItem> MainColumnItem { get; set; } public List<ColumnItem> MainColumnItem { get; set; }
public List<WindowSizeItem> WindowSizeItem { get; set; } public List<WindowSizeItem> WindowSizeItem { get; set; }
public bool HideColumnIpInfo { get; set; }
} }
[Serializable] [Serializable]
@@ -17,6 +17,8 @@ public record CoreConfigContext
// TUN Compatibility // TUN Compatibility
public bool IsTunEnabled { get; init; } = false; public bool IsTunEnabled { get; init; } = false;
public HashSet<string> ProtectDomainList { get; init; } = []; public HashSet<string> ProtectDomainList { get; init; } = [];
// Typically, it is the core of the outbound chain
public HashSet<ECoreType> ProtectCoreTypeList { get; init; } = [];
public bool IsWindows { get; init; } public bool IsWindows { get; init; }
public bool IsMacOS { get; init; } public bool IsMacOS { get; init; }
@@ -146,6 +146,16 @@ public class Outboundsettings4Ray
public int? port { get; set; } public int? port { get; set; }
public string? user { get; set; }
public string? pass { get; set; }
public int? level { get; set; }
public string? email { get; set; }
public object? headers { get; set; }
public List<WireguardPeer4Ray>? peers { get; set; } public List<WireguardPeer4Ray>? peers { get; set; }
public bool? noKernelTun { get; set; } public bool? noKernelTun { get; set; }
@@ -276,7 +286,6 @@ public class BalancersItem4Ray
public List<string>? selector { get; set; } public List<string>? selector { get; set; }
public BalancersStrategy4Ray? strategy { get; set; } public BalancersStrategy4Ray? strategy { get; set; }
public string? tag { get; set; } public string? tag { get; set; }
public string? fallbackTag { get; set; }
} }
public class BalancersStrategy4Ray public class BalancersStrategy4Ray
@@ -5,6 +5,9 @@ public record ProtocolExtraItem
public bool? Uot { get; init; } public bool? Uot { get; init; }
public string? CongestionControl { get; init; } public string? CongestionControl { get; init; }
// http outbound
public string? HttpHeaders { get; init; }
// vmess // vmess
public string? AlterId { get; init; } public string? AlterId { get; init; }
public string? VmessSecurity { get; init; } public string? VmessSecurity { get; init; }
+27
View File
@@ -321,6 +321,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Please enter valid HTTP request headers JSON. 的本地化字符串。
/// </summary>
public static string InvalidHttpOutboundHeaders {
get {
return ResourceManager.GetString("InvalidHttpOutboundHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Invalid Realm URL. 的本地化字符串。 /// 查找类似 Invalid Realm URL. 的本地化字符串。
/// </summary> /// </summary>
@@ -3240,6 +3249,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 HTTP headers 的本地化字符串。
/// </summary>
public static string TbHttpOutboundHeaders {
get {
return ResourceManager.GetString("TbHttpOutboundHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Realm URL 的本地化字符串。 /// 查找类似 Realm URL 的本地化字符串。
/// </summary> /// </summary>
@@ -4986,6 +5004,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom HTTP outbound request headers as a JSON object with string or string array values. 的本地化字符串。
/// </summary>
public static string TipHttpOutboundHeaders {
get {
return ResourceManager.GetString("TipHttpOutboundHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 *Default value raw 的本地化字符串。 /// 查找类似 *Default value raw 的本地化字符串。
/// </summary> /// </summary>
+10 -1
View File
@@ -1044,6 +1044,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>Congestion control</value> <value>Congestion control</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP headers</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>Custom HTTP outbound request headers as a JSON object with string or string array values.</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>Previous proxy remarks</value> <value>Previous proxy remarks</value>
</data> </data>
@@ -1806,6 +1812,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="InvalidHy2RealmUrl" xml:space="preserve"> <data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Invalid Realm URL.</value> <value>Invalid Realm URL.</value>
</data> </data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>Please enter valid HTTP request headers JSON.</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve"> <data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value> <value>Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data> </data>
@@ -1821,4 +1830,4 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbRootCertificateProviderTip" xml:space="preserve"> <data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>Only applies to the v2rayN GUI's downloads and network requests. Does not affect the core's certificate validation.</value> <value>Only applies to the v2rayN GUI's downloads and network requests. Does not affect the core's certificate validation.</value>
</data> </data>
</root> </root>
+60
View File
@@ -1044,6 +1044,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>Управление перегрузками</value> <value>Управление перегрузками</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP-заголовки</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк.</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>Псевдоним предыдущего прокси</value> <value>Псевдоним предыдущего прокси</value>
</data> </data>
@@ -1614,6 +1620,9 @@
<data name="TbEchConfigList" xml:space="preserve"> <data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value> <value>EchConfigList</value>
</data> </data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Проверять сертификат узла по имени</value>
</data>
<data name="TbFullCertTips" xml:space="preserve"> <data name="TbFullCertTips" xml:space="preserve">
<value>Полный сертификат (цепочка) в формате PEM</value> <value>Полный сертификат (цепочка) в формате PEM</value>
</data> </data>
@@ -1770,4 +1779,55 @@
<data name="menuNewUpdate" xml:space="preserve"> <data name="menuNewUpdate" xml:space="preserve">
<value>Доступно обновление</value> <value>Доступно обновление</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя.</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Адреса, исключаемые из маршрутизации</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>Разделяйте запятыми (,).</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>Конфликт между {0} и {1}</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>Включить финальную фрагментацию (Final Fragment)</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку.</value>
</data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS через Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM).</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Некорректный Realm URL.</value>
</data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>Введите корректный JSON заголовков HTTP-запроса.</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>Формат: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Размер пакета Gecko (мин/макс)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>Если включено, используется sing-box TUN; иначе — xray TUN.</value>
</data>
<data name="TbRootCertificateProvider" xml:space="preserve">
<value>Поставщик корневых сертификатов</value>
</data>
<data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>Применяется только к загрузкам и сетевым запросам графического интерфейса v2rayN. Не влияет на проверку сертификатов ядром.</value>
</data>
</root> </root>
+10 -1
View File
@@ -1041,6 +1041,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>拥塞控制算法</value> <value>拥塞控制算法</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP 请求头</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>自定义 HTTP 出站请求头,请输入值为字符串或字符串数组的 JSON 对象。</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>前置代理配置别名</value> <value>前置代理配置别名</value>
</data> </data>
@@ -1803,6 +1809,9 @@
<data name="InvalidHy2RealmUrl" xml:space="preserve"> <data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Realm URL 不正确。</value> <value>Realm URL 不正确。</value>
</data> </data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>请填写合法的HTTP请求头JSON。</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve"> <data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>格式:realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value> <value>格式:realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data> </data>
@@ -1818,4 +1827,4 @@
<data name="TbRootCertificateProviderTip" xml:space="preserve"> <data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>仅用于 v2rayN 界面程序的下载及网络请求,不影响核心的证书验证。</value> <value>仅用于 v2rayN 界面程序的下载及网络请求,不影响核心的证书验证。</value>
</data> </data>
</root> </root>
+31 -1
View File
@@ -1041,6 +1041,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>擁塞控制算法</value> <value>擁塞控制算法</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP 請求頭</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>自訂 HTTP 出站請求頭,請輸入值為字串或字串陣列的 JSON 物件。</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>前置代理節點別名</value> <value>前置代理節點別名</value>
</data> </data>
@@ -1794,7 +1800,31 @@
<data name="TbEnabletDnsViaProxy" xml:space="preserve"> <data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS 透過 Bridge</value> <value>DNS 透過 Bridge</value>
</data> </data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>偵測到不安全的設定:已啟用 AllowInsecure,但未提供憑證。這可能導致中間人攻擊(MITM)</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>無效的 Realm URL.</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>格式: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko 封包大小 (min/max)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve"> <data name="TbLegacyProtectTip" xml:space="preserve">
<value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value> <value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value>
</data> </data>
</root> <data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>請填寫合法的HTTP請求頭JSON。</value>
</data>
<data name="TbRootCertificateProvider" xml:space="preserve">
<value>根憑證提供者</value>
</data>
<data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>僅適用於 v2rayN GUI 的下載與網路請求,不影響核心的憑證驗證</value>
</data>
</root>
@@ -34,19 +34,22 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.AddRange(tunRules); _coreConfig.route.rules.AddRange(tunRules);
} }
var (lstDnsExe, lstDirectExe) = BuildRoutingDirectExe(); var lstDirectExe = BuildRoutingDirectExe();
_coreConfig.route.rules.Add(new() if (lstDirectExe.Count > 0)
{ {
port = [53], _coreConfig.route.rules.Add(new()
action = "hijack-dns", {
process_name = lstDnsExe port = [53],
}); action = "hijack-dns",
process_path = lstDirectExe,
});
_coreConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
outbound = Global.DirectTag, outbound = Global.DirectTag,
process_name = lstDirectExe process_path = lstDirectExe,
}); });
}
// ICMP Routing // ICMP Routing
var icmpRouting = _config.TunModeItem.IcmpRouting ?? ""; var icmpRouting = _config.TunModeItem.IcmpRouting ?? "";
@@ -256,34 +259,38 @@ public partial class CoreConfigSingboxService
} }
} }
private static (List<string> lstDnsExe, List<string> lstDirectExe) BuildRoutingDirectExe() private List<string> BuildRoutingDirectExe()
{ {
var dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo(); var allCoreInfo = CoreInfoManager.Instance.GetCoreInfo();
foreach (var coreConfig in coreInfoResult) foreach (var coreConfig in allCoreInfo)
{ {
if (!context.ProtectCoreTypeList.Contains(coreConfig.CoreType))
{
continue;
}
if (coreConfig.CoreType == ECoreType.v2rayN) if (coreConfig.CoreType == ECoreType.v2rayN)
{ {
continue; continue;
} }
if (coreConfig.CoreExes == null)
{
continue;
}
foreach (var baseExeName in coreConfig.CoreExes) foreach (var baseExeName in coreConfig.CoreExes)
{ {
if (coreConfig.CoreType != ECoreType.sing_box) //directExeSet.Add(Utils.GetExeName(baseExeName));
var exePath = CoreInfoManager.Instance.GetCoreExecFile(coreConfig, out _);
if (!exePath.IsNullOrEmpty())
{ {
dnsExeSet.Add(Utils.GetExeName(baseExeName)); directExeSet.Add(exePath);
} }
directExeSet.Add(Utils.GetExeName(baseExeName));
} }
} }
var lstDnsExe = new List<string>(dnsExeSet); return directExeSet.ToList();
var lstDirectExe = new List<string>(directExeSet);
return (lstDnsExe, lstDirectExe);
} }
private void GenRoutingUserRule(RulesItem? item) private void GenRoutingUserRule(RulesItem? item)
@@ -96,18 +96,19 @@ public partial class CoreConfigV2rayService
var balancer = new BalancersItem4Ray var balancer = new BalancersItem4Ray
{ {
selector = [selector], selector = [selector],
strategy = strategyType == "leastLoad" ? new() strategy = new()
{ {
type = strategyType, type = strategyType,
settings = new() settings = strategyType == "leastLoad"
{ ? new()
expected = 1, {
tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null, expected = 1,
maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null, tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null,
}, maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null,
} : null, }
: null,
},
tag = balancerTag, tag = balancerTag,
fallbackTag = _coreConfig.outbounds?.FirstOrDefault(o => o.tag.StartsWith(selector))?.tag,
}; };
_coreConfig.routing.balancers ??= []; _coreConfig.routing.balancers ??= [];
_coreConfig.routing.balancers.Add(balancer); _coreConfig.routing.balancers.Add(balancer);
@@ -74,6 +74,7 @@ public partial class CoreConfigV2rayService
tunInbound.settings.autoOutboundsInterface = bindInterface; tunInbound.settings.autoOutboundsInterface = bindInterface;
} }
tunInbound.sniffing = inbound.sniffing; tunInbound.sniffing = inbound.sniffing;
tunInbound.sniffing.routeOnly = true;
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 }) if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{ {
@@ -136,7 +136,6 @@ public partial class CoreConfigV2rayService
break; break;
} }
case EConfigType.SOCKS: case EConfigType.SOCKS:
case EConfigType.HTTP:
{ {
ServersItem4Ray serversItem; ServersItem4Ray serversItem;
if (outbound.settings.servers.Count <= 0) if (outbound.settings.servers.Count <= 0)
@@ -171,6 +170,31 @@ public partial class CoreConfigV2rayService
outbound.settings.vnext = null; outbound.settings.vnext = null;
break; break;
} }
case EConfigType.HTTP:
{
outbound.settings.address = _node.Address;
outbound.settings.port = _node.Port;
if (protocolExtra.HttpHeaders.IsNotEmpty())
{
outbound.settings.headers = JsonUtils.ParseJson(protocolExtra.HttpHeaders);
}
if (_node.Username.IsNotEmpty()
&& _node.Password.IsNotEmpty())
{
outbound.settings.user = _node.Username;
outbound.settings.pass = _node.Password;
outbound.settings.level = 1;
outbound.settings.email = Global.UserEMail;
}
FillOutboundMux(outbound);
outbound.settings.vnext = null;
outbound.settings.servers = null;
break;
}
case EConfigType.VLESS: case EConfigType.VLESS:
{ {
VnextItem4Ray vnextItem; VnextItem4Ray vnextItem;
@@ -13,18 +13,21 @@ public partial class CoreConfigV2rayService
{ {
_coreConfig.routing.rules.AddRange(tunRules); _coreConfig.routing.rules.AddRange(tunRules);
} }
var (lstDnsExe, lstDirectExe) = BuildRoutingDirectExe(); var lstDirectExe = BuildRoutingDirectExe();
_coreConfig.routing.rules.Add(new() if (lstDirectExe.Count > 0)
{ {
port = "53", _coreConfig.routing.rules.Add(new()
process = lstDnsExe, {
outboundTag = Global.DnsOutboundTag, port = "53",
}); process = lstDirectExe,
_coreConfig.routing.rules.Add(new() outboundTag = Global.DnsOutboundTag,
{ });
process = lstDirectExe, _coreConfig.routing.rules.Add(new()
outboundTag = Global.DirectTag, {
}); process = lstDirectExe,
outboundTag = Global.DirectTag,
});
}
_coreConfig.routing.rules.Add(new() _coreConfig.routing.rules.Add(new()
{ {
inboundTag = ["tun"], inboundTag = ["tun"],
@@ -232,36 +235,44 @@ public partial class CoreConfigV2rayService
return finalRule; return finalRule;
} }
private static (List<string> lstDnsExe, List<string> lstDirectExe) BuildRoutingDirectExe() private List<string> BuildRoutingDirectExe()
{ {
var dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo(); var allCoreInfo = CoreInfoManager.Instance.GetCoreInfo();
foreach (var coreConfig in coreInfoResult) foreach (var coreConfig in allCoreInfo)
{ {
if (!context.ProtectCoreTypeList.Contains(coreConfig.CoreType))
{
continue;
}
if (coreConfig.CoreType == ECoreType.v2rayN) if (coreConfig.CoreType == ECoreType.v2rayN)
{ {
continue; continue;
} }
if (coreConfig.CoreExes == null)
{
continue;
}
if (coreConfig.CoreType == ECoreType.Xray)
{
directExeSet.Add("xray/");
continue;
}
foreach (var baseExeName in coreConfig.CoreExes) foreach (var baseExeName in coreConfig.CoreExes)
{ {
if (coreConfig.CoreType != ECoreType.Xray) //directExeSet.Add(Utils.GetExeName(baseExeName));
var exePath = CoreInfoManager.Instance.GetCoreExecFile(coreConfig, out _);
if (!exePath.IsNullOrEmpty())
{ {
dnsExeSet.Add(Utils.GetExeName(baseExeName)); directExeSet.Add(exePath);
} }
directExeSet.Add(Utils.GetExeName(baseExeName));
} }
} }
//directExeSet.Add("xray/");
directExeSet.Add("xray/");
directExeSet.Add("self/"); directExeSet.Add("self/");
var lstDnsExe = new List<string>(dnsExeSet); return directExeSet.ToList();
var lstDirectExe = new List<string>(directExeSet);
return (lstDnsExe, lstDirectExe);
} }
} }
@@ -1,5 +1,4 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Reflection.Metadata;
namespace ServiceLib.Services; namespace ServiceLib.Services;
@@ -425,7 +425,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(it.IndexId, responseTime.ToString());
if (responseTime > 0) if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
{ {
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy); var ipInfo = await ConnectionHandler.GetIPInfo(webProxy);
var ipStr = ipInfo?.ToString() ?? Global.None; var ipStr = ipInfo?.ToString() ?? Global.None;
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class AddGroupServerViewModel : MyReactiveObject public class AddGroupServerViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public ProfileItem SelectedSource { get; set; }
@@ -29,7 +31,7 @@ public class AddGroupServerViewModel : MyReactiveObject
public IObservableCollection<ProfileItem> AllProfilePreviewItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>(); public IObservableCollection<ProfileItem> AllProfilePreviewItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>();
//public ReactiveCommand<Unit, Unit> AddCmd { get; } public ReactiveCommand<Unit, Unit> AddCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveCmd { get; } public ReactiveCommand<Unit, Unit> RemoveCmd { get; }
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; } public ReactiveCommand<Unit, Unit> MoveTopCmd { get; }
@@ -39,15 +41,18 @@ public class AddGroupServerViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public AddGroupServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView) public AddGroupServerViewModel(ProfileItem profileItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedChild, x => x.SelectedChild,
SelectedChild => SelectedChild != null && !SelectedChild.Remarks.IsNullOrEmpty()); selectedChild => selectedChild != null && !selectedChild.Remarks.IsNullOrEmpty());
AddCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddChildAsync();
});
RemoveCmd = ReactiveCommand.CreateFromTask(async () => RemoveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await ChildRemoveAsync(); await ChildRemoveAsync();
@@ -103,6 +108,20 @@ public class AddGroupServerViewModel : MyReactiveObject
ChildItemsObs.AddRange(childItemList); ChildItemsObs.AddRange(childItemList);
} }
public async Task AddChildAsync()
{
var profileSelectViewModel = new ProfilesSelectViewModel();
profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
profileSelectViewModel.MultiSelect = true;
var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel);
if (result != true)
{
return;
}
var profiles = await profileSelectViewModel.GetProfileItems() ?? [];
ChildItemsObs.AddRange(profiles);
}
public async Task ChildRemoveAsync() public async Task ChildRemoveAsync()
{ {
if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty())
@@ -230,7 +249,7 @@ public class AddGroupServerViewModel : MyReactiveObject
if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0) if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -1,7 +1,10 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class AddServer2ViewModel : MyReactiveObject public class AddServer2ViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
public Interaction<Unit, string?> BrowseConfigFileInteraction { get; } = new();
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public ProfileItem SelectedSource { get; set; }
@@ -13,15 +16,18 @@ public class AddServer2ViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveServerCmd { get; } public ReactiveCommand<Unit, Unit> SaveServerCmd { get; }
public bool IsModified { get; set; } public bool IsModified { get; set; }
public AddServer2ViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView) public AddServer2ViewModel(ProfileItem profileItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
BrowseServerCmd = ReactiveCommand.CreateFromTask(async () => BrowseServerCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
_updateView?.Invoke(EViewAction.BrowseServer, null); var fileName = await BrowseConfigFileInteraction.Handle(Unit.Default);
await Task.CompletedTask; if (fileName.IsNullOrEmpty())
{
return;
}
await BrowseServer(fileName);
}); });
EditServerCmd = ReactiveCommand.CreateFromTask(async () => EditServerCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -55,7 +61,7 @@ public class AddServer2ViewModel : MyReactiveObject
if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0) if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class AddServerViewModel : MyReactiveObject public class AddServerViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public ProfileItem SelectedSource { get; set; }
@@ -80,6 +82,9 @@ public class AddServerViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool NaiveQuic { get; set; } public bool NaiveQuic { get; set; }
[Reactive]
public string HttpHeadersJson { get; set; }
[Reactive] [Reactive]
public string Hy2RealmUrl { get; set; } public string Hy2RealmUrl { get; set; }
@@ -238,10 +243,9 @@ public class AddServerViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> FetchCertChainCmd { get; } public ReactiveCommand<Unit, Unit> FetchCertChainCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public AddServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView) public AddServerViewModel(ProfileItem profileItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
FetchCertCmd = ReactiveCommand.CreateFromTask(async () => FetchCertCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -255,7 +259,6 @@ public class AddServerViewModel : MyReactiveObject
{ {
await SaveServerAsync(); await SaveServerAsync();
}); });
this.WhenAnyValue(x => x.Cert) this.WhenAnyValue(x => x.Cert)
.Subscribe(_ => UpdateCertTip()); .Subscribe(_ => UpdateCertTip());
@@ -311,6 +314,7 @@ public class AddServerViewModel : MyReactiveObject
CongestionControl = protocolExtra.CongestionControl ?? string.Empty; CongestionControl = protocolExtra.CongestionControl ?? string.Empty;
InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null; InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
NaiveQuic = protocolExtra.NaiveQuic ?? false; NaiveQuic = protocolExtra.NaiveQuic ?? false;
HttpHeadersJson = protocolExtra.HttpHeaders ?? string.Empty;
Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty; Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty;
GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt(); GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt(); GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
@@ -379,6 +383,11 @@ public class AddServerViewModel : MyReactiveObject
return; return;
} }
} }
if (HttpHeadersJson.IsNotEmpty() && JsonUtils.ParseJson(HttpHeadersJson) == null)
{
NoticeManager.Instance.Enqueue(ResUI.InvalidHttpOutboundHeaders);
return;
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType); SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType);
SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse; SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse;
SelectedSource.MuxEnabled = MuxEnabled; SelectedSource.MuxEnabled = MuxEnabled;
@@ -416,6 +425,7 @@ public class AddServerViewModel : MyReactiveObject
VmessSecurity = VmessSecurity.NullIfEmpty(), VmessSecurity = VmessSecurity.NullIfEmpty(),
VlessEncryption = VlessEncryption.NullIfEmpty(), VlessEncryption = VlessEncryption.NullIfEmpty(),
SsMethod = SsMethod.NullIfEmpty(), SsMethod = SsMethod.NullIfEmpty(),
HttpHeaders = SelectedSource.ConfigType == EConfigType.HTTP ? HttpHeadersJson.NullIfEmpty() : null,
WgPublicKey = WgPublicKey.NullIfEmpty(), WgPublicKey = WgPublicKey.NullIfEmpty(),
WgPresharedKey = WgPresharedKey.NullIfEmpty(), WgPresharedKey = WgPresharedKey.NullIfEmpty(),
WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(), WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(),
@@ -434,7 +444,7 @@ public class AddServerViewModel : MyReactiveObject
if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) if (await ConfigHandler.AddServer(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -13,12 +13,11 @@ public class BackupAndRestoreViewModel : MyReactiveObject
public WebDavItem SelectedSource { get; set; } public WebDavItem SelectedSource { get; set; }
[Reactive] [Reactive]
public string OperationMsg { get; set; } public string OperationMsg { get; set; } = string.Empty;
public BackupAndRestoreViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public BackupAndRestoreViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () => WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -7,15 +7,16 @@ public class CheckUpdateViewModel : MyReactiveObject
private List<CheckUpdateModel> _lstUpdated = []; private List<CheckUpdateModel> _lstUpdated = [];
private static readonly string _tag = "CheckUpdateViewModel"; private static readonly string _tag = "CheckUpdateViewModel";
public EventChannel<Unit> ReloadRequested { get; } = new();
public IObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = new ObservableCollectionExtended<CheckUpdateModel>(); public IObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = new ObservableCollectionExtended<CheckUpdateModel>();
public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; } public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; }
public ReactiveCommand<Unit, Unit> CheckOnlyCmd { get; } public ReactiveCommand<Unit, Unit> CheckOnlyCmd { get; }
[Reactive] public bool EnableCheckPreReleaseUpdate { get; set; } [Reactive] public bool EnableCheckPreReleaseUpdate { get; set; }
public CheckUpdateViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public CheckUpdateViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
CheckUpdateCmd = ReactiveCommand.CreateFromTask(CheckUpdate); CheckUpdateCmd = ReactiveCommand.CreateFromTask(CheckUpdate);
CheckUpdateCmd.ThrownExceptions.Subscribe(ex => CheckUpdateCmd.ThrownExceptions.Subscribe(ex =>
@@ -299,7 +300,7 @@ public class CheckUpdateViewModel : MyReactiveObject
{ {
if (blReload) if (blReload)
{ {
AppEvents.ReloadRequested.Publish(); ReloadRequested.Publish();
} }
else else
{ {
@@ -16,10 +16,9 @@ public class ClashConnectionsViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool AutoRefresh { get; set; } public bool AutoRefresh { get; set; }
public ClashConnectionsViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ClashConnectionsViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh; AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
@@ -33,10 +33,9 @@ public class ClashProxiesViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool AutoRefresh { get; set; } public bool AutoRefresh { get; set; }
public ClashProxiesViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ClashProxiesViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
ProxiesReloadCmd = ReactiveCommand.CreateFromTask(async () => ProxiesReloadCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -86,15 +85,6 @@ public class ClashProxiesViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand #endregion WhenAnyValue && ReactiveCommand
#region AppEvents
AppEvents.ProxiesReloadRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await ProxiesReload());
#endregion AppEvents
_ = Init(); _ = Init();
} }
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class DNSSettingViewModel : MyReactiveObject public class DNSSettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
[Reactive] public bool? UseSystemHosts { get; set; } [Reactive] public bool? UseSystemHosts { get; set; }
[Reactive] public bool? AddCommonHosts { get; set; } [Reactive] public bool? AddCommonHosts { get; set; }
[Reactive] public bool? FakeIP { get; set; } [Reactive] public bool? FakeIP { get; set; }
@@ -17,15 +19,15 @@ public class DNSSettingViewModel : MyReactiveObject
[Reactive] public bool? ServeStale { get; set; } [Reactive] public bool? ServeStale { get; set; }
[Reactive] public bool UseSystemHostsCompatible { get; set; } [Reactive] public bool UseSystemHostsCompatible { get; set; }
[Reactive] public string DomainStrategy4FreedomCompatible { get; set; } [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } = string.Empty;
[Reactive] public string DomainDNSAddressCompatible { get; set; } [Reactive] public string DomainDNSAddressCompatible { get; set; } = string.Empty;
[Reactive] public string NormalDNSCompatible { get; set; } [Reactive] public string NormalDNSCompatible { get; set; } = string.Empty;
[Reactive] public string TunDNSCompatible { get; set; } [Reactive] public string TunDNSCompatible { get; set; } = string.Empty;
[Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } [Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } = string.Empty;
[Reactive] public string DomainDNSAddress2Compatible { get; set; } [Reactive] public string DomainDNSAddress2Compatible { get; set; } = string.Empty;
[Reactive] public string NormalDNS2Compatible { get; set; } [Reactive] public string NormalDNS2Compatible { get; set; } = string.Empty;
[Reactive] public string TunDNS2Compatible { get; set; } [Reactive] public string TunDNS2Compatible { get; set; } = string.Empty;
[Reactive] public bool RayCustomDNSEnableCompatible { get; set; } [Reactive] public bool RayCustomDNSEnableCompatible { get; set; }
[Reactive] public bool SBCustomDNSEnableCompatible { get; set; } [Reactive] public bool SBCustomDNSEnableCompatible { get; set; }
@@ -35,10 +37,9 @@ public class DNSSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; } public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; } public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; }
public DNSSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public DNSSettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(SaveSettingAsync); SaveCmd = ReactiveCommand.CreateFromTask(SaveSettingAsync);
ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () => ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -183,9 +184,6 @@ public class DNSSettingViewModel : MyReactiveObject
await ConfigHandler.SaveDNSItems(_config, item2); await ConfigHandler.SaveDNSItems(_config, item2);
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
if (_updateView != null) RequestClose?.Invoke(this, EventArgs.Empty);
{
await _updateView(EViewAction.CloseWindow, null);
}
} }
} }
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class FullConfigTemplateViewModel : MyReactiveObject public class FullConfigTemplateViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
#region Reactive #region Reactive
[Reactive] [Reactive]
@@ -11,16 +13,16 @@ public class FullConfigTemplateViewModel : MyReactiveObject
public bool EnableFullConfigTemplate4Singbox { get; set; } public bool EnableFullConfigTemplate4Singbox { get; set; }
[Reactive] [Reactive]
public string FullConfigTemplate4Ray { get; set; } public string FullConfigTemplate4Ray { get; set; } = string.Empty;
[Reactive] [Reactive]
public string FullTunConfigTemplate4Ray { get; set; } public string FullTunConfigTemplate4Ray { get; set; } = string.Empty;
[Reactive] [Reactive]
public string FullConfigTemplate4Singbox { get; set; } public string FullConfigTemplate4Singbox { get; set; } = string.Empty;
[Reactive] [Reactive]
public string FullTunConfigTemplate4Singbox { get; set; } public string FullTunConfigTemplate4Singbox { get; set; } = string.Empty;
[Reactive] [Reactive]
public bool AddProxyOnly4Ray { get; set; } public bool AddProxyOnly4Ray { get; set; }
@@ -29,19 +31,18 @@ public class FullConfigTemplateViewModel : MyReactiveObject
public bool AddProxyOnly4Singbox { get; set; } public bool AddProxyOnly4Singbox { get; set; }
[Reactive] [Reactive]
public string ProxyDetour4Ray { get; set; } public string ProxyDetour4Ray { get; set; } = string.Empty;
[Reactive] [Reactive]
public string ProxyDetour4Singbox { get; set; } public string ProxyDetour4Singbox { get; set; } = string.Empty;
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
#endregion Reactive #endregion Reactive
public FullConfigTemplateViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public FullConfigTemplateViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SaveSettingAsync(); await SaveSettingAsync();
@@ -84,7 +85,7 @@ public class FullConfigTemplateViewModel : MyReactiveObject
} }
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_ = _updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
private async Task<bool> SaveXrayConfigAsync() private async Task<bool> SaveXrayConfigAsync()
@@ -1,15 +1,16 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class GlobalHotkeySettingViewModel : MyReactiveObject public class GlobalHotkeySettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
private readonly List<KeyEventItem> _globalHotkeys; private readonly List<KeyEventItem> _globalHotkeys;
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public GlobalHotkeySettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public GlobalHotkeySettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
_globalHotkeys = JsonUtils.DeepCopy(_config.GlobalHotkeys); _globalHotkeys = JsonUtils.DeepCopy(_config.GlobalHotkeys);
@@ -51,7 +52,7 @@ public class GlobalHotkeySettingViewModel : MyReactiveObject
if (await ConfigHandler.SaveConfig(_config) == 0) if (await ConfigHandler.SaveConfig(_config) == 0)
{ {
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -4,6 +4,21 @@ namespace ServiceLib.ViewModels;
public class MainWindowViewModel : MyReactiveObject public class MainWindowViewModel : MyReactiveObject
{ {
public Interaction<Unit, string?> ReadTextFromClipboardInteraction { get; } = new();
public Interaction<Unit, byte[]?> ScanScreenInteraction { get; } = new();
public Interaction<Unit, string?> BrowseImageFileInteraction { get; } = new();
public Interaction<bool?, Unit> ShowHideWindowInteraction { get; } = new();
public bool DesignMode { get; set; }
public ProfilesViewModel ProfilesViewModel { get; } = new();
public MsgViewModel MsgViewModel { get; } = new();
public ClashProxiesViewModel ClashProxiesViewModel { get; } = new();
public ClashConnectionsViewModel ClashConnectionsViewModel { get; } = new();
public CheckUpdateViewModel CheckUpdateViewModel { get; } = new();
public BackupAndRestoreViewModel BackupAndRestoreViewModel { get; } = new();
public StatusBarViewModel StatusBarViewModel { get; } = StatusBarViewModel.Instance;
#region Menu #region Menu
//servers //servers
@@ -67,15 +82,17 @@ public class MainWindowViewModel : MyReactiveObject
[Reactive] public bool BlNewUpdate { get; set; } [Reactive] public bool BlNewUpdate { get; set; }
[Reactive] public EGirdOrientation MainGirdOrientation { get; set; }
#endregion Menu #endregion Menu
#region Init #region Init
public MainWindowViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public MainWindowViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
BlIsWindows = Utils.IsWindows(); BlIsWindows = Utils.IsWindows();
MainGirdOrientation = _config.UiItem.MainGirdOrientation;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
@@ -191,7 +208,8 @@ public class MainWindowViewModel : MyReactiveObject
}); });
GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () => GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true) var globalHotkeySettingViewModel = new GlobalHotkeySettingViewModel();
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(globalHotkeySettingViewModel) == true)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
} }
@@ -233,26 +251,11 @@ public class MainWindowViewModel : MyReactiveObject
#region AppEvents #region AppEvents
AppEvents.ReloadRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await Reload());
AppEvents.AddServerViaScanRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaScanAsync());
AppEvents.AddServerViaClipboardRequested AppEvents.AddServerViaClipboardRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaClipboardAsync(null)); .Subscribe(async _ => await AddServerViaClipboardAsync(null));
AppEvents.SubscriptionsUpdateRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
AppEvents.HasUpdateNotified AppEvents.HasUpdateNotified
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
@@ -260,6 +263,53 @@ public class MainWindowViewModel : MyReactiveObject
#endregion AppEvents #endregion AppEvents
ProfilesViewModel.RefreshServersRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshServers());
var vmReloadRequestedList = new List<IObservable<Unit>>
{
ProfilesViewModel.ReloadRequested.AsObservable(),
StatusBarViewModel.ReloadRequested.AsObservable(),
CheckUpdateViewModel.ReloadRequested.AsObservable(),
};
foreach (var reloadRequested in vmReloadRequestedList)
{
reloadRequested
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await Reload());
}
StatusBarViewModel.AddServerViaScanRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaScanAsync());
StatusBarViewModel.AddServerViaClipboardRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaClipboardAsync(null));
StatusBarViewModel.ShowHideWindowRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blShow =>
{
await ShowHideWindowInteraction.Handle(blShow);
});
StatusBarViewModel.SetDefaultServerRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async indexId => await ProfilesViewModel.SetDefaultServer(indexId));
StatusBarViewModel.SubscriptionsUpdateRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
_ = Init(); _ = Init();
} }
@@ -267,6 +317,11 @@ public class MainWindowViewModel : MyReactiveObject
{ {
AppManager.Instance.ShowInTaskbar = true; AppManager.Instance.ShowInTaskbar = true;
if (DesignMode)
{
return;
}
//await ConfigHandler.InitBuiltinRouting(_config); //await ConfigHandler.InitBuiltinRouting(_config);
await ConfigHandler.InitBuiltinDNS(_config); await ConfigHandler.InitBuiltinDNS(_config);
await ConfigHandler.InitBuiltinFullConfigTemplate(_config); await ConfigHandler.InitBuiltinFullConfigTemplate(_config);
@@ -279,7 +334,7 @@ public class MainWindowViewModel : MyReactiveObject
{ {
await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler); await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler);
} }
await RefreshServers(); await RefreshServersDispatcherAsync();
await Reload(); await Reload();
} }
@@ -304,7 +359,7 @@ public class MainWindowViewModel : MyReactiveObject
if (success) if (success)
{ {
var indexIdOld = _config.IndexId; var indexIdOld = _config.IndexId;
await RefreshServers(); await RefreshServersDispatcherAsync();
// If indexId changed or subIndexId is empty, directly reload. // If indexId changed or subIndexId is empty, directly reload.
if (indexIdOld != _config.IndexId || _config.SubIndexId.IsNullOrEmpty()) if (indexIdOld != _config.IndexId || _config.SubIndexId.IsNullOrEmpty())
@@ -323,7 +378,7 @@ public class MainWindowViewModel : MyReactiveObject
if (_config.UiItem.EnableAutoAdjustMainLvColWidth) if (_config.UiItem.EnableAutoAdjustMainLvColWidth)
{ {
AppEvents.AdjustMainLvColWidthRequested.Publish(); await ProfilesViewModel.AdjustMainLvColWidth();
} }
} }
} }
@@ -344,14 +399,20 @@ public class MainWindowViewModel : MyReactiveObject
private async Task RefreshServers() private async Task RefreshServers()
{ {
AppEvents.ProfilesRefreshRequested.Publish(); await ProfilesViewModel.RefreshServersBiz();
await StatusBarViewModel.RefreshServersBiz();
await Task.Delay(200); // await Task.Delay(200);
} }
private void RefreshSubscriptions() private async Task RefreshServersDispatcherAsync()
{ {
AppEvents.SubscriptionsRefreshRequested.Publish(); await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
}
private async Task RefreshSubscriptions()
{
await Observable.Start(async () => await ProfilesViewModel.RefreshSubscriptions(), RxSchedulers.MainThreadScheduler);
} }
#endregion Servers && Groups #endregion Servers && Groups
@@ -370,19 +431,22 @@ public class MainWindowViewModel : MyReactiveObject
bool? ret = false; bool? ret = false;
if (eConfigType == EConfigType.Custom) if (eConfigType == EConfigType.Custom)
{ {
ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); var addServer2ViewModel = new AddServer2ViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel);
} }
else if (eConfigType.IsGroupType()) else if (eConfigType.IsGroupType())
{ {
ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); var addGroupServerViewModel = new AddGroupServerViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addGroupServerViewModel);
} }
else else
{ {
ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); var addServerViewModel = new AddServerViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServerViewModel);
} }
if (ret == true) if (ret == true)
{ {
await RefreshServers(); await RefreshServersDispatcherAsync();
if (item.IndexId == _config.IndexId) if (item.IndexId == _config.IndexId)
{ {
await Reload(); await Reload();
@@ -392,16 +456,22 @@ public class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaClipboardAsync(string? clipboardData) public async Task AddServerViaClipboardAsync(string? clipboardData)
{ {
var stringData = clipboardData;
if (clipboardData == null) if (clipboardData == null)
{ {
await _updateView?.Invoke(EViewAction.AddServerViaClipboard, null); var result = await ReadTextFromClipboardInteraction.Handle(Unit.Default);
return; if (result.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return;
}
stringData = result;
} }
var ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false); var ret = await ConfigHandler.AddBatchServers(_config, stringData, _config.SubIndexId, false);
if (ret > 0) if (ret > 0)
{ {
RefreshSubscriptions(); await RefreshSubscriptions();
await RefreshServers(); await RefreshServersDispatcherAsync();
NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret)); NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret));
} }
else else
@@ -412,8 +482,8 @@ public class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaScanAsync() public async Task AddServerViaScanAsync()
{ {
_updateView?.Invoke(EViewAction.ScanScreenTask, null); var result = await ScanScreenInteraction.Handle(Unit.Default);
await Task.CompletedTask; await ScanScreenResult(result);
} }
public async Task ScanScreenResult(byte[]? bytes) public async Task ScanScreenResult(byte[]? bytes)
@@ -424,8 +494,8 @@ public class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaImageAsync() public async Task AddServerViaImageAsync()
{ {
_updateView?.Invoke(EViewAction.ScanImageTask, null); var imageFileName = await BrowseImageFileInteraction.Handle(Unit.Default);
await Task.CompletedTask; await AddScanResultAsync(imageFileName);
} }
public async Task ScanImageResult(string fileName) public async Task ScanImageResult(string fileName)
@@ -450,8 +520,8 @@ public class MainWindowViewModel : MyReactiveObject
var ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false); var ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false);
if (ret > 0) if (ret > 0)
{ {
RefreshSubscriptions(); await RefreshSubscriptions();
await RefreshServers(); await RefreshServersDispatcherAsync();
NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan); NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan);
} }
else else
@@ -467,9 +537,10 @@ public class MainWindowViewModel : MyReactiveObject
private async Task SubSettingAsync() private async Task SubSettingAsync()
{ {
if (await _updateView?.Invoke(EViewAction.SubSettingWindow, null) == true) var subSettingViewModel = new SubSettingViewModel();
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subSettingViewModel) == true)
{ {
RefreshSubscriptions(); await RefreshSubscriptions();
} }
} }
@@ -484,28 +555,38 @@ public class MainWindowViewModel : MyReactiveObject
private async Task OptionSettingAsync() private async Task OptionSettingAsync()
{ {
var ret = await _updateView?.Invoke(EViewAction.OptionSettingWindow, null); var settingViewModel = new OptionSettingViewModel();
var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(settingViewModel);
if (ret == true) if (ret == true)
{ {
AppEvents.InboundDisplayRequested.Publish(); MainGirdOrientation = _config.UiItem.MainGirdOrientation;
RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await StatusBarViewModel.InboundDisplayStatus();
});
await Reload(); await Reload();
} }
} }
private async Task RoutingSettingAsync() private async Task RoutingSettingAsync()
{ {
var ret = await _updateView?.Invoke(EViewAction.RoutingSettingWindow, null); var routingSettingViewModel = new RoutingSettingViewModel();
var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(routingSettingViewModel);
if (ret == true) if (ret == true)
{ {
await ConfigHandler.InitBuiltinRouting(_config); await ConfigHandler.InitBuiltinRouting(_config);
AppEvents.RoutingsMenuRefreshRequested.Publish(); RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await StatusBarViewModel.RefreshRoutingsMenu();
});
await Reload(); await Reload();
} }
} }
private async Task DNSSettingAsync() private async Task DNSSettingAsync()
{ {
var ret = await _updateView?.Invoke(EViewAction.DNSSettingWindow, null); var dnsSettingViewModel = new DNSSettingViewModel();
var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(dnsSettingViewModel);
if (ret == true) if (ret == true)
{ {
await Reload(); await Reload();
@@ -514,7 +595,8 @@ public class MainWindowViewModel : MyReactiveObject
private async Task FullConfigTemplateAsync() private async Task FullConfigTemplateAsync()
{ {
var ret = await _updateView?.Invoke(EViewAction.FullConfigTemplateWindow, null); var fullConfigTemplateViewModel = new FullConfigTemplateViewModel();
var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(fullConfigTemplateViewModel);
if (ret == true) if (ret == true)
{ {
await Reload(); await Reload();
@@ -524,7 +606,7 @@ public class MainWindowViewModel : MyReactiveObject
private async Task ClearServerStatistics() private async Task ClearServerStatistics()
{ {
await StatisticsManager.Instance.ClearAllServerStatistics(); await StatisticsManager.Instance.ClearAllServerStatistics();
await RefreshServers(); await RefreshServersDispatcherAsync();
} }
private async Task OpenTheFileLocation() private async Task OpenTheFileLocation()
@@ -561,6 +643,12 @@ public class MainWindowViewModel : MyReactiveObject
return; return;
} }
if (DesignMode)
{
_reloadSemaphore.Release();
return;
}
try try
{ {
SetReloadEnabled(false); SetReloadEnabled(false);
@@ -583,12 +671,22 @@ public class MainWindowViewModel : MyReactiveObject
await SysProxyHandler.UpdateSysProxy(_config, false); await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000); await Task.Delay(1000);
}); });
AppEvents.TestServerRequested.Publish(); RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await StatusBarViewModel.TestServerAvailability();
});
var showClashUI = AppManager.Instance.IsRunningCore(ECoreType.sing_box); var showClashUI = AppManager.Instance.IsRunningCore(ECoreType.sing_box);
if (showClashUI) if (showClashUI)
{ {
AppEvents.ProxiesReloadRequested.Publish(); //await Observable.Start(async () =>
//{
// await ClashProxiesViewModel.ProxiesReload();
//}, RxSchedulers.MainThreadScheduler);
RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await ClashProxiesViewModel.ProxiesReload();
});
} }
ReloadResult(showClashUI); ReloadResult(showClashUI);
@@ -633,7 +731,10 @@ public class MainWindowViewModel : MyReactiveObject
{ {
await ConfigHandler.ApplyRegionalPreset(_config, type); await ConfigHandler.ApplyRegionalPreset(_config, type);
await ConfigHandler.InitRouting(_config); await ConfigHandler.InitRouting(_config);
AppEvents.RoutingsMenuRefreshRequested.Publish(); RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await StatusBarViewModel.RefreshRoutingsMenu();
});
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
await new UpdateService(_config, UpdateTaskHandler).UpdateGeoFileAll(); await new UpdateService(_config, UpdateTaskHandler).UpdateGeoFileAll();
+4 -3
View File
@@ -2,6 +2,8 @@ namespace ServiceLib.ViewModels;
public class MsgViewModel : MyReactiveObject public class MsgViewModel : MyReactiveObject
{ {
public Interaction<string, Unit> DispatcherShowMsgInteraction { get; } = new();
private readonly ConcurrentQueue<string> _queueMsg = new(); private readonly ConcurrentQueue<string> _queueMsg = new();
private volatile bool _lastMsgFilterNotAvailable; private volatile bool _lastMsgFilterNotAvailable;
private int _showLock = 0; // 0 = unlocked, 1 = locked private int _showLock = 0; // 0 = unlocked, 1 = locked
@@ -13,10 +15,9 @@ public class MsgViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool AutoRefresh { get; set; } public bool AutoRefresh { get; set; }
public MsgViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public MsgViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
MsgFilter = _config.MsgUIItem.MainMsgFilter ?? string.Empty; MsgFilter = _config.MsgUIItem.MainMsgFilter ?? string.Empty;
AutoRefresh = _config.MsgUIItem.AutoRefresh ?? true; AutoRefresh = _config.MsgUIItem.AutoRefresh ?? true;
@@ -64,7 +65,7 @@ public class MsgViewModel : MyReactiveObject
sb.Append(line); sb.Append(line);
} }
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString()); await DispatcherShowMsgInteraction.Handle(sb.ToString());
} }
finally finally
{ {
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class OptionSettingViewModel : MyReactiveObject public class OptionSettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
#region Core #region Core
[Reactive] public int LocalPort { get; set; } [Reactive] public int LocalPort { get; set; }
@@ -123,10 +125,9 @@ public class OptionSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public OptionSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public OptionSettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
BlIsWindows = Utils.IsWindows(); BlIsWindows = Utils.IsWindows();
BlIsLinux = Utils.IsLinux(); BlIsLinux = Utils.IsLinux();
BlIsIsMacOS = Utils.IsMacOS(); BlIsIsMacOS = Utils.IsMacOS();
@@ -142,8 +143,6 @@ public class OptionSettingViewModel : MyReactiveObject
private async Task Init() private async Task Init()
{ {
await _updateView?.Invoke(EViewAction.InitSettingFont, null);
#region Core #region Core
var inbound = _config.Inbound.First(); var inbound = _config.Inbound.First();
@@ -314,8 +313,7 @@ public class OptionSettingViewModel : MyReactiveObject
|| DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed || DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed
|| EnableDragDropSort != _config.UiItem.EnableDragDropSort || EnableDragDropSort != _config.UiItem.EnableDragDropSort
|| EnableHWA != _config.GuiItem.EnableHWA || EnableHWA != _config.GuiItem.EnableHWA
|| CurrentFontFamily != _config.UiItem.CurrentFontFamily || CurrentFontFamily != _config.UiItem.CurrentFontFamily;
|| MainGirdOrientation != (int)_config.UiItem.MainGirdOrientation;
//if (Utile.IsNullOrEmpty(Kcpmtu.ToString()) || !Utile.IsNumeric(Kcpmtu.ToString()) //if (Utile.IsNullOrEmpty(Kcpmtu.ToString()) || !Utile.IsNumeric(Kcpmtu.ToString())
// || Utile.IsNullOrEmpty(Kcptti.ToString()) || !Utile.IsNumeric(Kcptti.ToString()) // || Utile.IsNullOrEmpty(Kcptti.ToString()) || !Utile.IsNumeric(Kcptti.ToString())
@@ -432,7 +430,7 @@ public class OptionSettingViewModel : MyReactiveObject
AppManager.Instance.Reset(); AppManager.Instance.Reset();
NoticeManager.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -1,7 +1,10 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class ProfilesSelectViewModel : MyReactiveObject public class ProfilesSelectViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
public Interaction<Unit, Unit> ProfilesFocusInteraction { get; } = new();
#region private prop #region private prop
private string _serverFilter = string.Empty; private string _serverFilter = string.Empty;
@@ -12,6 +15,8 @@ public class ProfilesSelectViewModel : MyReactiveObject
#endregion private prop #endregion private prop
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
#region ObservableCollection #region ObservableCollection
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>(); public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
@@ -36,18 +41,25 @@ public class ProfilesSelectViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool FilterExclude { get; set; } public bool FilterExclude { get; set; }
[Reactive]
public bool MultiSelect { get; set; }
#endregion ObservableCollection #endregion ObservableCollection
#region Init #region Init
public ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ProfilesSelectViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
_subIndexId = _config.SubIndexId ?? string.Empty; _subIndexId = _config.SubIndexId ?? string.Empty;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
SaveCmd = ReactiveCommand.Create(() =>
{
SelectFinish();
});
this.WhenAnyValue( this.WhenAnyValue(
x => x.SelectedSub, x => x.SelectedSub,
y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id) y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id)
@@ -107,7 +119,7 @@ public class ProfilesSelectViewModel : MyReactiveObject
{ {
return false; return false;
} }
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
return true; return true;
} }
@@ -125,7 +137,13 @@ public class ProfilesSelectViewModel : MyReactiveObject
await RefreshServers(); await RefreshServers();
await _updateView?.Invoke(EViewAction.ProfilesFocus, null); try
{
await ProfilesFocusInteraction.Handle(Unit.Default);
}
catch (UnhandledInteractionException<Unit, Unit>)
{
}
} }
private async Task ServerFilterChanged(bool c) private async Task ServerFilterChanged(bool c)
@@ -157,8 +175,6 @@ public class ProfilesSelectViewModel : MyReactiveObject
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
SelectedProfile = selected ?? lstModel.First(); SelectedProfile = selected ?? lstModel.First();
} }
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
} }
private async Task RefreshSubscriptions() private async Task RefreshSubscriptions()
@@ -2,6 +2,17 @@ namespace ServiceLib.ViewModels;
public class ProfilesViewModel : MyReactiveObject public class ProfilesViewModel : MyReactiveObject
{ {
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<ProfileItem, bool> SaveFileDialogInteraction { get; } = new();
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, Unit> ProfilesFocusInteraction { get; } = new();
public Interaction<string, Unit> ShareServerInteraction { get; } = new();
public Interaction<Unit, Unit> DispatcherRefreshServersBizInteraction { get; } = new();
public Interaction<Unit, Unit> AdjustMainLvColWidthInteraction { get; } = new();
public EventChannel<Unit> ReloadRequested { get; } = new();
public EventChannel<Unit> RefreshServersRequested { get; } = new();
#region private prop #region private prop
private List<ProfileItem> _lstProfile; private List<ProfileItem> _lstProfile;
@@ -82,10 +93,9 @@ public class ProfilesViewModel : MyReactiveObject
#region Init #region Init
public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ProfilesViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
@@ -236,26 +246,11 @@ public class ProfilesViewModel : MyReactiveObject
#region AppEvents #region AppEvents
AppEvents.ProfilesRefreshRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshServersBiz());
AppEvents.SubscriptionsRefreshRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshSubscriptions());
AppEvents.DispatcherStatisticsRequested AppEvents.DispatcherStatisticsRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result)); .Subscribe(async result => await UpdateStatistics(result));
AppEvents.SetDefaultServerRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async indexId => await SetDefaultServer(indexId));
#endregion AppEvents #endregion AppEvents
_ = Init(); _ = Init();
@@ -277,7 +272,7 @@ public class ProfilesViewModel : MyReactiveObject
private void Reload() private void Reload()
{ {
AppEvents.ReloadRequested.Publish(); ReloadRequested.Publish();
} }
public async Task SetSpeedTestResult(SpeedTestResult result) public async Task SetSpeedTestResult(SpeedTestResult result)
@@ -350,7 +345,13 @@ public class ProfilesViewModel : MyReactiveObject
await RefreshServers(); await RefreshServers();
await _updateView?.Invoke(EViewAction.ProfilesFocus, null); try
{
await ProfilesFocusInteraction.Handle(Unit.Default);
}
catch (UnhandledInteractionException<Unit, Unit>)
{
}
} }
private async Task ServerFilterChanged(bool c) private async Task ServerFilterChanged(bool c)
@@ -368,19 +369,21 @@ public class ProfilesViewModel : MyReactiveObject
public async Task RefreshServers() public async Task RefreshServers()
{ {
AppEvents.ProfilesRefreshRequested.Publish(); RefreshServersRequested.Publish();
await Task.Delay(200); // await Task.Delay(200);
await Task.CompletedTask;
} }
private 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)) ?? []; _lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
ProfileItems.Clear(); ProfileItems.Clear();
ProfileItems.AddRange(lstModel); ProfileItems.AddRange(lstModel ?? []);
if (lstModel.Count > 0) if (lstModel?.Count > 0)
{ {
ProfileItemModel? selected = null; ProfileItemModel? selected = null;
if (!_pendingSelectIndexId.IsNullOrEmpty()) if (!_pendingSelectIndexId.IsNullOrEmpty())
@@ -392,10 +395,16 @@ public class ProfilesViewModel : MyReactiveObject
SelectedProfile = selected ?? lstModel.First(); SelectedProfile = selected ?? lstModel.First();
} }
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); try
{
await DispatcherRefreshServersBizInteraction.Handle(Unit.Default);
}
catch (UnhandledInteractionException<Unit, Unit>)
{
}
} }
private async Task RefreshSubscriptions() public async Task RefreshSubscriptions()
{ {
var subItems = await AppManager.Instance.SubItems(); var subItems = await AppManager.Instance.SubItems();
subItems.Insert(0, new SubItem { Remarks = ResUI.AllGroupServers }); subItems.Insert(0, new SubItem { Remarks = ResUI.AllGroupServers });
@@ -408,6 +417,11 @@ public class ProfilesViewModel : MyReactiveObject
: null) ?? subItems.FirstOrDefault(); : null) ?? subItems.FirstOrDefault();
} }
public async Task AdjustMainLvColWidth()
{
await AdjustMainLvColWidthInteraction.Handle(Unit.Default);
}
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(_config.SubIndexId, filter); var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, filter);
@@ -491,15 +505,18 @@ public class ProfilesViewModel : MyReactiveObject
bool? ret = false; bool? ret = false;
if (eConfigType == EConfigType.Custom) if (eConfigType == EConfigType.Custom)
{ {
ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); var addServer2ViewModel = new AddServer2ViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel);
} }
else if (eConfigType.IsGroupType()) else if (eConfigType.IsGroupType())
{ {
ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); var addGroupServerViewModel = new AddGroupServerViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addGroupServerViewModel);
} }
else else
{ {
ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); var addServerViewModel = new AddServerViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServerViewModel);
} }
if (ret == true) if (ret == true)
{ {
@@ -518,7 +535,7 @@ public class ProfilesViewModel : MyReactiveObject
{ {
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -539,7 +556,7 @@ public class ProfilesViewModel : MyReactiveObject
private async Task RemoveDuplicateServer() private async Task RemoveDuplicateServer()
{ {
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -576,7 +593,7 @@ public class ProfilesViewModel : MyReactiveObject
await SetDefaultServer(SelectedProfile.IndexId); await SetDefaultServer(SelectedProfile.IndexId);
} }
private async Task SetDefaultServer(string? indexId) public async Task SetDefaultServer(string? indexId)
{ {
if (indexId.IsNullOrEmpty()) if (indexId.IsNullOrEmpty())
{ {
@@ -614,7 +631,7 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
await _updateView?.Invoke(EViewAction.ShareServer, url); await ShareServerInteraction.Handle(url);
} }
private async Task GenGroupAllServer() private async Task GenGroupAllServer()
@@ -783,13 +800,13 @@ public class ProfilesViewModel : MyReactiveObject
} }
else else
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, result.Data); await SetClipboardDataInteraction.Handle((string)result.Data);
NoticeManager.Instance.SendMessage(ResUI.OperationSuccess); NoticeManager.Instance.SendMessage(ResUI.OperationSuccess);
} }
} }
else else
{ {
await _updateView?.Invoke(EViewAction.SaveFileDialog, item); await SaveFileDialogInteraction.Handle(item);
} }
} }
@@ -838,11 +855,11 @@ public class ProfilesViewModel : MyReactiveObject
{ {
if (blEncode) if (blEncode)
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, Utils.Base64Encode(sb.ToString())); await SetClipboardDataInteraction.Handle(Utils.Base64Encode(sb.ToString()));
} }
else else
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); await SetClipboardDataInteraction.Handle(sb.ToString());
} }
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
} }
@@ -865,7 +882,7 @@ public class ProfilesViewModel : MyReactiveObject
if (!result.IsNullOrEmpty()) if (!result.IsNullOrEmpty())
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, result); await SetClipboardDataInteraction.Handle(result);
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
} }
else else
@@ -893,7 +910,8 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
} }
if (await _updateView?.Invoke(EViewAction.SubEditWindow, item) == true) var subEditViewModel = new SubEditViewModel(item);
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subEditViewModel) == true)
{ {
await RefreshSubscriptions(); await RefreshSubscriptions();
await SubSelectedChangedAsync(true); await SubSelectedChangedAsync(true);
@@ -908,7 +926,7 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class RoutingRuleDetailsViewModel : MyReactiveObject public class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
public IList<string> ProtocolItems { get; set; } public IList<string> ProtocolItems { get; set; }
public IList<string> InboundTagItems { get; set; } public IList<string> InboundTagItems { get; set; }
@@ -23,13 +25,17 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool AutoSort { get; set; } public bool AutoSort { get; set; }
public ReactiveCommand<Unit, Unit> SelectProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public RoutingRuleDetailsViewModel(RulesItem rulesItem, Func<EViewAction, object?, Task<bool>>? updateView) public RoutingRuleDetailsViewModel(RulesItem rulesItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
SelectProfileCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SelectProfileAsync();
});
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SaveRulesAsync(); await SaveRulesAsync();
@@ -88,6 +94,24 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
return; return;
} }
//NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); //NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess);
await _updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
await Task.CompletedTask;
}
private async Task SelectProfileAsync()
{
var profileSelectViewModel = new ProfilesSelectViewModel();
profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel);
if (result != true)
{
return;
}
var profileItem = await profileSelectViewModel.GetProfileItem();
if (profileItem != null)
{
SelectedSource.OutboundTag = profileItem.Remarks;
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
}
} }
} }
@@ -1,7 +1,13 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class RoutingRuleSettingViewModel : MyReactiveObject public class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, string?> ReadTextFromClipboardInteraction { get; } = new();
public Interaction<Unit, string?> BrowseRulesFileInteraction { get; } = new();
private List<RulesItem> _rules; private List<RulesItem> _rules;
[Reactive] [Reactive]
@@ -27,10 +33,9 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public RoutingRuleSettingViewModel(RoutingItem routingItem, Func<EViewAction, object?, Task<bool>>? updateView) public RoutingRuleSettingViewModel(RoutingItem routingItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedSource, x => x.SelectedSource,
@@ -42,7 +47,8 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
}); });
ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () => ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await _updateView?.Invoke(EViewAction.ImportRulesFromFile, null); var fileName = await BrowseRulesFileInteraction.Handle(Unit.Default);
await ImportRulesFromFileAsync(fileName);
}); });
ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () => ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -131,7 +137,8 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
return; return;
} }
} }
if (await _updateView?.Invoke(EViewAction.RoutingRuleDetailsWindow, item) == true) var routingRuleDetailsViewModel = new RoutingRuleDetailsViewModel(item);
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(routingRuleDetailsViewModel) == true)
{ {
if (blNew) if (blNew)
{ {
@@ -148,7 +155,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -191,7 +198,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}; };
await _updateView?.Invoke(EViewAction.SetClipboardData, JsonUtils.Serialize(lst, options)); await SetClipboardDataInteraction.Handle(JsonUtils.Serialize(lst, options));
} }
} }
@@ -234,7 +241,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
if (await ConfigHandler.SaveRoutingItem(_config, item) == 0) if (await ConfigHandler.SaveRoutingItem(_config, item) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -266,12 +273,18 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
public async Task ImportRulesFromClipboardAsync(string? clipboardData) public async Task ImportRulesFromClipboardAsync(string? clipboardData)
{ {
var stringData = clipboardData;
if (clipboardData == null) if (clipboardData == null)
{ {
await _updateView?.Invoke(EViewAction.ImportRulesFromClipboard, null); var result = await ReadTextFromClipboardInteraction.Handle(Unit.Default);
return; if (result.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return;
}
stringData = result;
} }
var ret = await AddBatchRoutingRulesAsync(SelectedRouting, clipboardData); var ret = await AddBatchRoutingRulesAsync(SelectedRouting, stringData);
if (ret == 0) if (ret == 0)
{ {
RefreshRulesItems(); RefreshRulesItems();
@@ -301,7 +314,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
private async Task<int> AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData) private async Task<int> AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData)
{ {
var blReplace = false; var blReplace = false;
if (await _updateView?.Invoke(EViewAction.AddBatchRoutingRulesYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.AddBatchRoutingRulesYesNo) == false)
{ {
blReplace = true; blReplace = true;
} }
@@ -2,6 +2,8 @@ namespace ServiceLib.ViewModels;
public class RoutingSettingViewModel : MyReactiveObject public class RoutingSettingViewModel : MyReactiveObject
{ {
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
#region Reactive #region Reactive
public IObservableCollection<RoutingItemModel> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItemModel>(); public IObservableCollection<RoutingItemModel> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItemModel>();
@@ -26,10 +28,9 @@ public class RoutingSettingViewModel : MyReactiveObject
#endregion Reactive #endregion Reactive
public RoutingSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public RoutingSettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedSource, x => x.SelectedSource,
@@ -131,7 +132,8 @@ public class RoutingSettingViewModel : MyReactiveObject
return; return;
} }
} }
if (await _updateView?.Invoke(EViewAction.RoutingRuleSettingWindow, item) == true) var routingRuleSettingViewModel = new RoutingRuleSettingViewModel(item);
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(routingRuleSettingViewModel) == true)
{ {
await RefreshRoutingItems(); await RefreshRoutingItems();
IsModified = true; IsModified = true;
@@ -145,7 +147,7 @@ public class RoutingSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -2,9 +2,20 @@ namespace ServiceLib.ViewModels;
public class StatusBarViewModel : MyReactiveObject public class StatusBarViewModel : MyReactiveObject
{ {
private static readonly Lazy<StatusBarViewModel> _instance = new(() => new(null)); public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, string?> PasswordInputInteraction { get; } = new();
public Interaction<Unit, Unit> DispatcherRefreshIconInteraction { get; } = new();
public EventChannel<bool> SubscriptionsUpdateRequested { get; } = new();
public EventChannel<bool?> ShowHideWindowRequested { get; } = new();
private static readonly Lazy<StatusBarViewModel> _instance = new(() => new());
public static StatusBarViewModel Instance => _instance.Value; public static StatusBarViewModel Instance => _instance.Value;
public EventChannel<string> SetDefaultServerRequested { get; } = new();
public EventChannel<Unit> ReloadRequested { get; } = new();
public EventChannel<Unit> AddServerViaScanRequested { get; } = new();
public EventChannel<Unit> AddServerViaClipboardRequested { get; } = new();
#region ObservableCollection #region ObservableCollection
public IObservableCollection<RoutingItem> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItem>(); public IObservableCollection<RoutingItem> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItem>();
@@ -92,12 +103,12 @@ public class StatusBarViewModel : MyReactiveObject
#endregion UI #endregion UI
public StatusBarViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public StatusBarViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
SelectedRouting = new(); SelectedRouting = new();
SelectedServer = new(); SelectedServer = new();
RunningServerToolTipText = "-"; RunningServerToolTipText = GetRunningServerToolTipText("-");
BlSystemProxyPacVisible = Utils.IsWindows(); BlSystemProxyPacVisible = Utils.IsWindows();
BlIsNonWindows = Utils.IsNonWindows(); BlIsNonWindows = Utils.IsNonWindows();
@@ -140,17 +151,17 @@ public class StatusBarViewModel : MyReactiveObject
NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () => NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
AppEvents.ShowHideWindowRequested.Publish(null); ShowHideWindowRequested.Publish(null);
await Task.CompletedTask; await Task.CompletedTask;
}); });
ShowWindowCmd = ReactiveCommand.CreateFromTask(async () => ShowWindowCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
AppEvents.ShowHideWindowRequested.Publish(true); ShowHideWindowRequested.Publish(true);
await Task.CompletedTask; await Task.CompletedTask;
}); });
HideWindowCmd = ReactiveCommand.CreateFromTask(async () => HideWindowCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
AppEvents.ShowHideWindowRequested.Publish(false); ShowHideWindowRequested.Publish(false);
await Task.CompletedTask; await Task.CompletedTask;
}); });
@@ -193,31 +204,11 @@ public class StatusBarViewModel : MyReactiveObject
#region AppEvents #region AppEvents
if (updateView != null)
{
InitUpdateView(updateView);
}
AppEvents.DispatcherStatisticsRequested AppEvents.DispatcherStatisticsRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result)); .Subscribe(async result => await UpdateStatistics(result));
AppEvents.RoutingsMenuRefreshRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshRoutingsMenu());
AppEvents.TestServerRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await TestServerAvailability());
AppEvents.InboundDisplayRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await InboundDisplayStatus());
AppEvents.SysProxyChangeRequested AppEvents.SysProxyChangeRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
@@ -238,18 +229,6 @@ public class StatusBarViewModel : MyReactiveObject
BlRouting = true; BlRouting = true;
} }
public void InitUpdateView(Func<EViewAction, object?, Task<bool>>? updateView)
{
_updateView = updateView;
if (_updateView != null)
{
AppEvents.ProfilesRefreshRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshServersBiz()); //.DisposeWith(_disposables);
}
}
private async Task CopyProxyCmdToClipboard() private async Task CopyProxyCmdToClipboard()
{ {
var cmd = Utils.IsWindows() ? "set" : "export"; var cmd = Utils.IsWindows() ? "set" : "export";
@@ -264,28 +243,28 @@ public class StatusBarViewModel : MyReactiveObject
sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}"); sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}");
sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}"); sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}");
await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); await SetClipboardDataInteraction.Handle(sb.ToString());
} }
private async Task AddServerViaClipboard() private async Task AddServerViaClipboard()
{ {
AppEvents.AddServerViaClipboardRequested.Publish(); AddServerViaClipboardRequested.Publish();
await Task.Delay(1000); await Task.Delay(1000);
} }
private async Task AddServerViaScan() private async Task AddServerViaScan()
{ {
AppEvents.AddServerViaScanRequested.Publish(); AddServerViaScanRequested.Publish();
await Task.Delay(1000); await Task.Delay(1000);
} }
private async Task UpdateSubscriptionProcess(bool blProxy) private async Task UpdateSubscriptionProcess(bool blProxy)
{ {
AppEvents.SubscriptionsUpdateRequested.Publish(blProxy); SubscriptionsUpdateRequested.Publish(blProxy);
await Task.Delay(1000); await Task.Delay(1000);
} }
private async Task RefreshServersBiz() public async Task RefreshServersBiz()
{ {
await RefreshServersMenu(); await RefreshServersMenu();
@@ -293,22 +272,26 @@ public class StatusBarViewModel : MyReactiveObject
var running = await ConfigHandler.GetDefaultServer(_config); var running = await ConfigHandler.GetDefaultServer(_config);
if (running != null) if (running != null)
{ {
RunningServerDisplay = RunningServerDisplay = running.GetSummary();
RunningServerToolTipText = running.GetSummary(); RunningServerToolTipText = GetRunningServerToolTipText(RunningServerDisplay);
} }
else else
{ {
RunningServerDisplay = RunningServerDisplay = ResUI.CheckServerSettings;
RunningServerToolTipText = ResUI.CheckServerSettings; RunningServerToolTipText = GetRunningServerToolTipText(RunningServerDisplay);
} }
} }
private string GetRunningServerToolTipText(string serverInfo)
{
return Utils.IsLinux() ? Global.AppName : serverInfo;
}
private async Task RefreshServersMenu() private async Task RefreshServersMenu()
{ {
var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, ""); var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, "");
Servers.Clear(); if (lstModel?.Count > _config.GuiItem.TrayMenuServersLimit)
if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit)
{ {
BlServers = false; BlServers = false;
return; return;
@@ -327,6 +310,7 @@ public class StatusBarViewModel : MyReactiveObject
SelectedServer = item; SelectedServer = item;
} }
} }
Servers.Clear();
Servers.AddRange(models); Servers.AddRange(models);
} }
@@ -344,7 +328,7 @@ public class StatusBarViewModel : MyReactiveObject
{ {
return; return;
} }
AppEvents.SetDefaultServerRequested.Publish(SelectedServer.ID); SetDefaultServerRequested.Publish(SelectedServer.ID);
} }
public async Task TestServerAvailability() public async Task TestServerAvailability()
@@ -406,11 +390,18 @@ public class StatusBarViewModel : MyReactiveObject
if (blChange) if (blChange)
{ {
_updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); try
{
await DispatcherRefreshIconInteraction.Handle(Unit.Default);
}
catch (UnhandledInteractionException<Unit, Unit>)
{
// Ignore
}
} }
} }
private async Task RefreshRoutingsMenu() public async Task RefreshRoutingsMenu()
{ {
var routings = await AppManager.Instance.RoutingItems(); var routings = await AppManager.Instance.RoutingItems();
@@ -441,8 +432,8 @@ public class StatusBarViewModel : MyReactiveObject
if (await ConfigHandler.SetDefaultRouting(_config, item) == 0) if (await ConfigHandler.SetDefaultRouting(_config, item) == 0)
{ {
NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting); NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
AppEvents.ReloadRequested.Publish(); ReloadRequested.Publish();
_updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); await DispatcherRefreshIconInteraction.Handle(Unit.Default);
} }
} }
@@ -479,8 +470,8 @@ public class StatusBarViewModel : MyReactiveObject
} }
else else
{ {
bool? passwordResult = await _updateView?.Invoke(EViewAction.PasswordInput, null); var password = await PasswordInputInteraction.Handle(Unit.Default);
if (passwordResult == false) if (password.IsNullOrEmpty())
{ {
_config.TunModeItem.EnableTun = false; _config.TunModeItem.EnableTun = false;
return; return;
@@ -489,7 +480,7 @@ public class StatusBarViewModel : MyReactiveObject
} }
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
AppEvents.ReloadRequested.Publish(); ReloadRequested.Publish();
} }
private bool AllowEnableTun() private bool AllowEnableTun()
@@ -513,7 +504,7 @@ public class StatusBarViewModel : MyReactiveObject
#region UI #region UI
private async Task InboundDisplayStatus() public async Task InboundDisplayStatus()
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}"); sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}");
@@ -1,17 +1,38 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class SubEditViewModel : MyReactiveObject public class SubEditViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
[Reactive] [Reactive]
public SubItem SelectedSource { get; set; } public SubItem SelectedSource { get; set; }
public ReactiveCommand<Unit, Unit> SelectPrevProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SelectNextProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public SubEditViewModel(SubItem subItem, Func<EViewAction, object?, Task<bool>>? updateView) public SubEditViewModel(SubItem subItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
SelectPrevProfileCmd = ReactiveCommand.CreateFromTask(async () =>
{
var profileItem = await SelectProfileAsync();
if (profileItem != null)
{
SelectedSource?.PrevProfile = profileItem.Remarks;
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
}
});
SelectNextProfileCmd = ReactiveCommand.CreateFromTask(async () =>
{
var profileItem = await SelectProfileAsync();
if (profileItem != null)
{
SelectedSource?.NextProfile = profileItem.Remarks;
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
}
});
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SaveSubAsync(); await SaveSubAsync();
@@ -49,11 +70,24 @@ public class SubEditViewModel : MyReactiveObject
if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0) if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
private async Task<ProfileItem?> SelectProfileAsync()
{
var profileSelectViewModel = new ProfilesSelectViewModel();
profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel);
if (result != true)
{
return null;
}
var profileItem = await profileSelectViewModel.GetProfileItem();
return profileItem;
}
} }
@@ -2,6 +2,9 @@ namespace ServiceLib.ViewModels;
public class SubSettingViewModel : MyReactiveObject public class SubSettingViewModel : MyReactiveObject
{ {
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<string, Unit> ShareSubInteraction { get; } = new();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>(); public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
[Reactive] [Reactive]
@@ -15,10 +18,9 @@ public class SubSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SubShareCmd { get; } public ReactiveCommand<Unit, Unit> SubShareCmd { get; }
public bool IsModified { get; set; } public bool IsModified { get; set; }
public SubSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public SubSettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedSource, x => x.SelectedSource,
@@ -38,7 +40,7 @@ public class SubSettingViewModel : MyReactiveObject
}, canEditRemove); }, canEditRemove);
SubShareCmd = ReactiveCommand.CreateFromTask(async () => SubShareCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await _updateView?.Invoke(EViewAction.ShareSub, SelectedSource?.Url); await ShareSubInteraction.Handle(SelectedSource?.Url);
}, canEditRemove); }, canEditRemove);
_ = Init(); _ = Init();
@@ -72,7 +74,8 @@ public class SubSettingViewModel : MyReactiveObject
return; return;
} }
} }
if (await _updateView?.Invoke(EViewAction.SubEditWindow, item) == true) var subEditViewModel = new SubEditViewModel(item);
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subEditViewModel) == true)
{ {
await RefreshSubItems(); await RefreshSubItems();
IsModified = true; IsModified = true;
@@ -81,7 +84,7 @@ public class SubSettingViewModel : MyReactiveObject
private async Task DeleteSubAsync() private async Task DeleteSubAsync()
{ {
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
+1 -1
View File
@@ -8,7 +8,7 @@
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Name="v2rayN" Name="v2rayN"
x:DataType="vms:StatusBarViewModel" x:DataType="vms:StatusBarViewModel"
RequestedThemeVariant="Default"> RequestedThemeVariant="Default">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
+6 -1
View File
@@ -15,6 +15,9 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
var viewLocator = SimpleViewLocator.Instance;
DataTemplates.Add(viewLocator);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
if (!Design.IsDesignMode) if (!Design.IsDesignMode)
@@ -23,7 +26,9 @@ public partial class App : Application
DataContext = StatusBarViewModel.Instance; DataContext = StatusBarViewModel.Instance;
} }
var mainWindow = new MainWindow(); var mainWindowViewModel = new MainWindowViewModel();
var mainWindow = (MainWindow)viewLocator.Build(mainWindowViewModel);
mainWindow.ViewModel = mainWindowViewModel;
desktop.MainWindow = mainWindow; desktop.MainWindow = mainWindow;
if (OperatingSystem.IsMacOS()) if (OperatingSystem.IsMacOS())
+12 -14
View File
@@ -4,7 +4,6 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
{ {
public WindowBase() public WindowBase()
{ {
Initialized += OnWindowInitialized;
Loaded += OnLoaded; Loaded += OnLoaded;
Loaded += (s, e) => Loaded += (s, e) =>
{ {
@@ -20,7 +19,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
throw new NotImplementedException(); throw new NotImplementedException();
} }
private void OnWindowInitialized(object? sender, EventArgs e) protected virtual void OnLoaded(object? sender, RoutedEventArgs e)
{ {
try try
{ {
@@ -30,23 +29,22 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
return; return;
} }
if (sizeItem.Width > 0 && !Width.Equals(sizeItem.Width)) var screen = Screens.ScreenFromWindow(this) ?? Screens.Primary;
{ var scaling = screen.Scaling > 0 ? screen.Scaling : 1.0;
Width = sizeItem.Width; var workingArea = screen.WorkingArea;
}
if (sizeItem.Height > 0 && !Height.Equals(sizeItem.Height)) var width = Math.Min(sizeItem.Width, workingArea.Width / scaling);
{ var height = Math.Min(sizeItem.Height, workingArea.Height / scaling);
Height = sizeItem.Height; var x = workingArea.X + ((workingArea.Width - (width * scaling)) / 2);
} var y = workingArea.Y + ((workingArea.Height - (height * scaling)) / 2);
Width = width;
Height = height;
Position = new PixelPoint((int)x, (int)y);
} }
catch { } catch { }
} }
protected virtual void OnLoaded(object? sender, RoutedEventArgs e)
{
}
protected override void OnClosed(EventArgs e) protected override void OnClosed(EventArgs e)
{ {
base.OnClosed(e); base.OnClosed(e);
@@ -0,0 +1,69 @@
using Avalonia.Controls.Templates;
using v2rayN.Desktop.ViewModels;
using v2rayN.Desktop.Views;
namespace v2rayN.Desktop.Common;
public class SimpleViewLocator : IDataTemplate
{
private static readonly Lazy<SimpleViewLocator> _instance = new(() => new SimpleViewLocator());
private readonly Dictionary<Type, Func<Control?>> _locator = new();
private SimpleViewLocator()
{
RegisterViewFactory<AddGroupServerViewModel, AddGroupServerWindow>();
RegisterViewFactory<AddServer2ViewModel, AddServer2Window>();
RegisterViewFactory<AddServerViewModel, AddServerWindow>();
RegisterViewFactory<BackupAndRestoreViewModel, BackupAndRestoreView>();
RegisterViewFactory<CheckUpdateViewModel, CheckUpdateView>();
RegisterViewFactory<ClashConnectionsViewModel, ClashConnectionsView>();
RegisterViewFactory<ClashProxiesViewModel, ClashProxiesView>();
RegisterViewFactory<DNSSettingViewModel, DNSSettingWindow>();
RegisterViewFactory<FullConfigTemplateViewModel, FullConfigTemplateWindow>();
RegisterViewFactory<GlobalHotkeySettingViewModel, GlobalHotkeySettingWindow>();
RegisterViewFactory<MainWindowViewModel, MainWindow>();
RegisterViewFactory<MsgViewModel, MsgView>();
RegisterViewFactory<OptionSettingViewModel, OptionSettingWindow>();
RegisterViewFactory<ProfilesSelectViewModel, ProfilesSelectWindow>();
RegisterViewFactory<ProfilesViewModel, ProfilesView>();
RegisterViewFactory<RoutingRuleDetailsViewModel, RoutingRuleDetailsWindow>();
RegisterViewFactory<RoutingRuleSettingViewModel, RoutingRuleSettingWindow>();
RegisterViewFactory<RoutingSettingViewModel, RoutingSettingWindow>();
RegisterViewFactory<StatusBarViewModel, StatusBarView>();
RegisterViewFactory<SubEditViewModel, SubEditWindow>();
RegisterViewFactory<SubSettingViewModel, SubSettingWindow>();
RegisterViewFactory<ThemeSettingViewModel, ThemeSettingView>();
}
public static SimpleViewLocator Instance => _instance.Value;
public Control Build(object? data)
{
if (data is null)
{
return new TextBlock { Text = "No VM provided" };
}
_locator.TryGetValue(data.GetType(), out var factory);
return factory?.Invoke() ?? new TextBlock { Text = $"VM Not Registered: {data.GetType()}" };
}
public bool Match(object? data)
{
return data is MyReactiveObject;
}
public void RegisterViewFactory<TViewModel>(Func<Control> factory) where TViewModel : class
{
_locator.Add(typeof(TViewModel), factory);
}
public void RegisterViewFactory<TViewModel, TView>()
where TViewModel : class
where TView : Control, new()
{
_locator.Add(typeof(TViewModel), () => new TView());
}
}
+9 -6
View File
@@ -1,4 +1,5 @@
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using v2rayN.Desktop.Manager;
using v2rayN.Desktop.Views; using v2rayN.Desktop.Views;
namespace v2rayN.Desktop.Common; namespace v2rayN.Desktop.Common;
@@ -7,16 +8,17 @@ internal class UI
{ {
private static readonly string caption = Global.AppName; private static readonly string caption = Global.AppName;
public static async Task<ButtonResult> ShowYesNo(Window owner, string msg) public static async Task<ButtonResult> ShowYesNo(string msg)
{ {
var owner = WindowDialog.TryGetOwnerWindow();
var box = new MessageBoxDialog(caption, msg); var box = new MessageBoxDialog(caption, msg);
var result = await box.ShowDialog<ButtonResult>(owner); var result = await box.ShowDialog<ButtonResult>(owner);
return result == ButtonResult.Yes ? ButtonResult.Yes : ButtonResult.No; return result == ButtonResult.Yes ? ButtonResult.Yes : ButtonResult.No;
} }
public static async Task<string?> OpenFileDialog(Window owner, FilePickerFileType? filter) public static async Task<string?> OpenFileDialog(FilePickerFileType? filter)
{ {
var sp = GetStorageProvider(owner); var sp = GetStorageProvider();
if (sp is null) if (sp is null)
{ {
return null; return null;
@@ -32,9 +34,9 @@ internal class UI
return files.FirstOrDefault()?.TryGetLocalPath(); return files.FirstOrDefault()?.TryGetLocalPath();
} }
public static async Task<string?> SaveFileDialog(Window owner, string filter) public static async Task<string?> SaveFileDialog(string filter)
{ {
var sp = GetStorageProvider(owner); var sp = GetStorageProvider();
if (sp is null) if (sp is null)
{ {
return null; return null;
@@ -48,8 +50,9 @@ internal class UI
return files?.TryGetLocalPath(); return files?.TryGetLocalPath();
} }
private static IStorageProvider? GetStorageProvider(Window owner) private static IStorageProvider? GetStorageProvider()
{ {
var owner = WindowDialog.TryGetOwnerWindow();
var topLevel = TopLevel.GetTopLevel(owner); var topLevel = TopLevel.GetTopLevel(owner);
return topLevel?.StorageProvider; return topLevel?.StorageProvider;
} }
@@ -0,0 +1,99 @@
using ServiceLib.Models.Entities;
using System.Diagnostics;
using v2rayN.Desktop.Manager;
using v2rayN.Desktop.ViewModels;
namespace v2rayN.Desktop.DesignData;
/// <summary>
/// Provides design-time data for Avalonia XAML previewer.
/// Each inner class lazily initializes <see cref="AppManager"/> with a stub config
/// so that ViewModel constructors don't fail during design-time rendering.
/// </summary>
public static class DesignData
{
// ── Parameterless-constructor ViewModels ───────────────────────────────
public static MainWindowViewModel? MainWindow { get; } = SafeCreate(CreateMainWindow);
public static ProfilesViewModel? Profiles { get; } = SafeCreate(() => new ProfilesViewModel());
public static StatusBarViewModel? StatusBar { get; } = SafeCreate(CreateStatusBar);
public static MsgViewModel? Msg { get; } = SafeCreate(() => new MsgViewModel());
public static SubSettingViewModel? SubSetting { get; } = SafeCreate(() => new SubSettingViewModel());
public static RoutingSettingViewModel? RoutingSetting { get; } = SafeCreate(() => new RoutingSettingViewModel());
public static ClashProxiesViewModel? ClashProxies { get; } = SafeCreate(() => new ClashProxiesViewModel());
public static ClashConnectionsViewModel? ClashConnections { get; } = SafeCreate(() => new ClashConnectionsViewModel());
public static CheckUpdateViewModel? CheckUpdate { get; } = SafeCreate(() => new CheckUpdateViewModel());
public static DNSSettingViewModel? DNSSetting { get; } = SafeCreate(() => new DNSSettingViewModel());
public static FullConfigTemplateViewModel? FullConfigTemplate { get; } = SafeCreate(() => new FullConfigTemplateViewModel());
public static GlobalHotkeySettingViewModel? GlobalHotkeySetting { get; } = SafeCreate(() => new GlobalHotkeySettingViewModel());
public static OptionSettingViewModel? OptionSetting { get; } = SafeCreate(() => new OptionSettingViewModel());
public static ProfilesSelectViewModel? ProfilesSelect { get; } = SafeCreate(() => new ProfilesSelectViewModel());
public static BackupAndRestoreViewModel? BackupAndRestore { get; } = SafeCreate(() => new BackupAndRestoreViewModel());
public static ThemeSettingViewModel? ThemeSetting { get; } = SafeCreate(() => new ThemeSettingViewModel());
// ── ViewModels that require constructor parameters ─────────────────────
public static AddGroupServerViewModel? AddGroupServer { get; } = SafeCreate(() => new AddGroupServerViewModel(new ProfileItem { Remarks = "Design Group", ConfigType = EConfigType.PolicyGroup }));
public static AddServer2ViewModel? AddServer2 { get; } = SafeCreate(() => new AddServer2ViewModel(new ProfileItem { Remarks = "Design Custom Server", ConfigType = EConfigType.Custom }));
public static AddServerViewModel? AddServer { get; } = SafeCreate(() => new AddServerViewModel(new ProfileItem { Remarks = "Design VMess Server", ConfigType = EConfigType.VMess, Address = "example.com", Port = 443 }));
public static RoutingRuleSettingViewModel? RoutingRuleSetting { get; } = SafeCreate(() => new RoutingRuleSettingViewModel(new RoutingItem { Remarks = "Design Routing Rule" }));
public static RoutingRuleDetailsViewModel? RoutingRuleDetails { get; } = SafeCreate(() => new RoutingRuleDetailsViewModel(new RulesItem { Domain = ["example.com"], OutboundTag = "direct" }));
public static SubEditViewModel? SubEdit { get; } = SafeCreate(() => new SubEditViewModel(new SubItem { Remarks = "Design Subscription", Url = "https://example.com/sub" }));
// ── Helper factories ───────────────────────────────────────────────────
private static MainWindowViewModel CreateMainWindow()
{
var vm = new MainWindowViewModel { DesignMode = true };
return vm;
}
private static StatusBarViewModel CreateStatusBar()
{
var vm = StatusBarViewModel.Instance;
vm.InboundDisplay = "socks:10808";
vm.InboundLanDisplay = "http:10809";
vm.RunningServerDisplay = "🚀 Design Server (Active)";
vm.RunningInfoDisplay = "v2rayN Design Mode";
vm.SpeedProxyDisplay = "↑ 1.2 MB/s";
vm.SpeedDirectDisplay = "↓ 5.6 MB/s";
vm.RoutingItems.Add(new RoutingItem { Remarks = "Default Routing" });
vm.RoutingItems.Add(new RoutingItem { Remarks = "Global" });
return vm;
}
private static T? SafeCreate<T>(Func<T> factory) where T : class
{
try
{
AppManager.Instance.InitApp();
AppManager.Instance.WindowDialog = new WindowDialog();
return factory();
}
catch (Exception ex)
{
Debug.WriteLine($"[DesignData] Failed to create {typeof(T).Name}: {ex}");
return null;
}
}
}
+1
View File
@@ -3,6 +3,7 @@ global using System.Collections.Generic;
global using System.Globalization; global using System.Globalization;
global using System.IO; global using System.IO;
global using System.Linq; global using System.Linq;
global using System.Reactive;
global using System.Reactive.Disposables.Fluent; global using System.Reactive.Disposables.Fluent;
global using System.Reactive.Linq; global using System.Reactive.Linq;
global using System.Runtime.Versioning; global using System.Runtime.Versioning;
@@ -0,0 +1,68 @@
using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Manager;
public class WindowDialog : IWindowDialog
{
public async Task<bool> ShowDialogAsync<TViewModel>(TViewModel vm)
where TViewModel : class
{
var owner = TryGetOwnerWindow();
var view = SimpleViewLocator.Instance.Build(vm);
if (view is not WindowBase<TViewModel> window)
{
return false;
}
window.ViewModel = vm;
if (vm is ServiceLib.Base.ICloseable closeable)
{
closeable.RequestClose += (_, _) => Dispatch(() => window.Close(true));
}
var result = await window.ShowDialog<bool>(owner);
return result;
}
public static Window TryGetOwnerWindow()
{
var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime
?? throw new InvalidOperationException("Application lifetime is not of type IClassicDesktopStyleApplicationLifetime.");
var openWindows = desktop.Windows
.Where(w => w.IsVisible)
.ToArray();
if (openWindows.Length == 0)
{
return desktop.MainWindow
?? throw new InvalidOperationException("No open windows and no main window found.");
}
if (openWindows.Length == 1)
{
return openWindows[0];
}
try
{
Window.SortWindowsByZOrder(openWindows);
var activeTopmost = openWindows.Reverse().FirstOrDefault(w => w.IsActive);
return activeTopmost ?? openWindows[^1];
}
catch
{
return desktop.Windows.FirstOrDefault(w => w.IsActive)
?? desktop.MainWindow
?? openWindows[0];
}
}
private static void Dispatch(Action action)
{
Dispatcher.UIThread.InvokeAsync(action);
}
}
+6
View File
@@ -1,4 +1,5 @@
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
using v2rayN.Desktop.Manager;
namespace v2rayN.Desktop; namespace v2rayN.Desktop;
@@ -48,6 +49,8 @@ internal class Program
{ {
return false; return false;
} }
AppManager.Instance.WindowDialog = new WindowDialog();
return true; return true;
} }
@@ -58,6 +61,9 @@ internal class Program
.UsePlatformDetect() .UsePlatformDetect()
//.WithInterFont() //.WithInterFont()
.WithFontByDefault() .WithFontByDefault()
#if DEBUG
.WithDeveloperTools()
#endif
.LogToTrace() .LogToTrace()
.UseReactiveUI(_ => { }); .UseReactiveUI(_ => { });
@@ -3,6 +3,7 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
@@ -10,6 +11,7 @@
Width="900" Width="900"
Height="700" Height="700"
x:DataType="vms:AddGroupServerViewModel" x:DataType="vms:AddGroupServerViewModel"
Design.DataContext="{x:Static dd:DesignData.AddGroupServer}"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -5,11 +5,6 @@ namespace v2rayN.Desktop.Views;
public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel> public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
{ {
public AddGroupServerWindow() public AddGroupServerWindow()
{
InitializeComponent();
}
public AddGroupServerWindow(ProfileItem profileItem)
{ {
InitializeComponent(); InitializeComponent();
@@ -18,8 +13,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
lstChild.SelectionChanged += LstChild_SelectionChanged; lstChild.SelectionChanged += LstChild_SelectionChanged;
tabControl.SelectionChanged += TabControl_SelectionChanged; tabControl.SelectionChanged += TabControl_SelectionChanged;
ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler);
cmbCoreType.ItemsSource = Global.CoreTypes; cmbCoreType.ItemsSource = Global.CoreTypes;
cmbPolicyGroupType.ItemsSource = new List<string> cmbPolicyGroupType.ItemsSource = new List<string>
{ {
@@ -31,6 +24,42 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
}; };
cmbFilter.ItemsSource = Global.PolicyGroupDefaultFilterList; cmbFilter.ItemsSource = Global.PolicyGroupDefaultFilterList;
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables);
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbSubChildItems.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.cmbFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCmd, v => v.menuAddChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
this.WhenAnyValue(v => v.ViewModel.SelectedSource)
.WhereNotNull()
.Subscribe(InitializeData)
.DisposeWith(disposables);
});
// Context menu actions that require custom logic (Add, SelectAll)
menuSelectAllChild.Click += (s, e) => lstChild.SelectAll();
// Keyboard shortcuts when focus is within grid
AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel);
lstChild.LoadingRow += LstChild_LoadingRow;
}
private void InitializeData(ProfileItem profileItem)
{
switch (profileItem.ConfigType) switch (profileItem.ConfigType)
{ {
case EConfigType.PolicyGroup: case EConfigType.PolicyGroup:
@@ -46,34 +75,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
} }
break; break;
} }
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables);
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbSubChildItems.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.cmbFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
// Context menu actions that require custom logic (Add, SelectAll)
menuAddChildServer.Click += MenuAddChild_Click;
menuSelectAllChild.Click += (s, e) => lstChild.SelectAll();
// Keyboard shortcuts when focus is within grid
AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel);
lstChild.LoadingRow += LstChild_LoadingRow;
} }
private void LstChild_LoadingRow(object? sender, DataGridRowEventArgs e) private void LstChild_LoadingRow(object? sender, DataGridRowEventArgs e)
@@ -81,17 +82,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
e.Row.Header = $" {e.Row.Index + 1}"; e.Row.Header = $" {e.Row.Index + 1}";
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
}
return await Task.FromResult(true);
}
private void Window_Loaded(object? sender, RoutedEventArgs e) private void Window_Loaded(object? sender, RoutedEventArgs e)
{ {
txtRemarks.Focus(); txtRemarks.Focus();
@@ -145,19 +135,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
} }
} }
private async void MenuAddChild_Click(object? sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
selectWindow.AllowMultiSelect(true);
var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true)
{
var profiles = await selectWindow.ProfileItems;
ViewModel?.ChildItemsObs.AddRange(profiles);
}
}
private void LstChild_SelectionChanged(object? sender, SelectionChangedEventArgs e) private void LstChild_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{ {
if (ViewModel != null) if (ViewModel != null)
@@ -3,6 +3,7 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
@@ -10,6 +11,7 @@
Width="700" Width="700"
Height="500" Height="500"
x:DataType="vms:AddServer2ViewModel" x:DataType="vms:AddServer2ViewModel"
Design.DataContext="{x:Static dd:DesignData.AddServer2}"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -6,17 +6,11 @@ namespace v2rayN.Desktop.Views;
public partial class AddServer2Window : WindowBase<AddServer2ViewModel> public partial class AddServer2Window : WindowBase<AddServer2ViewModel>
{ {
public AddServer2Window() public AddServer2Window()
{
InitializeComponent();
}
public AddServer2Window(ProfileItem profileItem)
{ {
InitializeComponent(); InitializeComponent();
Loaded += Window_Loaded; Loaded += Window_Loaded;
btnCancel.Click += (s, e) => Close(); btnCancel.Click += (s, e) => Close();
ViewModel = new AddServer2ViewModel(profileItem, UpdateViewHandler);
cmbCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty(); cmbCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty();
@@ -31,30 +25,15 @@ public partial class AddServer2Window : WindowBase<AddServer2ViewModel>
this.BindCommand(ViewModel, vm => vm.BrowseServerCmd, v => v.btnBrowse).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.BrowseServerCmd, v => v.btnBrowse).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.EditServerCmd, v => v.btnEdit).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.EditServerCmd, v => v.btnEdit).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveServerCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveServerCmd, v => v.btnSave).DisposeWith(disposables);
ViewModel.BrowseConfigFileInteraction.RegisterHandler(async interaction =>
{
var fileName = await UI.OpenFileDialog(null);
interaction.SetOutput(fileName);
}).DisposeWith(disposables);
}); });
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
case EViewAction.BrowseServer:
var fileName = await UI.OpenFileDialog(this, null);
if (fileName.IsNullOrEmpty())
{
return false;
}
ViewModel?.BrowseServer(fileName);
break;
}
return await Task.FromResult(true);
}
private void Window_Loaded(object? sender, RoutedEventArgs e) private void Window_Loaded(object? sender, RoutedEventArgs e)
{ {
txtRemarks.Focus(); txtRemarks.Focus();
@@ -3,6 +3,7 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:views="clr-namespace:v2rayN.Desktop.Views" xmlns:views="clr-namespace:v2rayN.Desktop.Views"
@@ -11,6 +12,7 @@
Width="900" Width="900"
Height="600" Height="600"
x:DataType="vms:AddServerViewModel" x:DataType="vms:AddServerViewModel"
Design.DataContext="{x:Static dd:DesignData.AddServer}"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -256,6 +258,27 @@
Grid.Column="1" Grid.Column="1"
Width="400" Width="400"
Margin="{StaticResource Margin4}" /> Margin="{StaticResource Margin4}" />
<TextBlock
x:Name="tbHttpHeaders"
Grid.Row="3"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
IsVisible="False"
Text="{x:Static resx:ResUI.TbHttpOutboundHeaders}"
ToolTip.Tip="{x:Static resx:ResUI.TipHttpOutboundHeaders}" />
<views:JsonEditor
x:Name="txtHttpHeadersJson"
Grid.Row="3"
Grid.Column="1"
Width="400"
MinHeight="100"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="False"
ToolTip.Tip="{x:Static resx:ResUI.TipHttpOutboundHeaders}" />
</Grid> </Grid>
<Grid <Grid
x:Name="gridVLESS" x:Name="gridVLESS"
@@ -422,12 +445,12 @@
x:Name="txtMinGeckoPacketSize" x:Name="txtMinGeckoPacketSize"
Width="90" Width="90"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="Min" /> PlaceholderText="Min" />
<TextBox <TextBox
x:Name="txtMaxGeckoPacketSize" x:Name="txtMaxGeckoPacketSize"
Width="90" Width="90"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="Max" /> PlaceholderText="Max" />
</StackPanel> </StackPanel>
<TextBlock <TextBlock
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -460,7 +483,7 @@
Grid.Column="1" Grid.Column="1"
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="1000-2000,3000,4000" /> PlaceholderText="1000-2000,3000,4000" />
<TextBlock <TextBlock
Grid.Row="2" Grid.Row="2"
Grid.Column="2" Grid.Column="2"
@@ -498,12 +521,12 @@
x:Name="txtUpMbps7" x:Name="txtUpMbps7"
Width="90" Width="90"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="Up" /> PlaceholderText="Up" />
<TextBox <TextBox
x:Name="txtDownMbps7" x:Name="txtDownMbps7"
Width="90" Width="90"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="Down" /> PlaceholderText="Down" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<Grid <Grid
@@ -612,7 +635,7 @@
Grid.Column="1" Grid.Column="1"
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="0, 0, 0" /> PlaceholderText="0, 0, 0" />
<TextBlock <TextBlock
Grid.Row="5" Grid.Row="5"
@@ -626,7 +649,7 @@
Grid.Column="1" Grid.Column="1"
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="Ipv4,Ipv6" /> PlaceholderText="Ipv4,Ipv6" />
<TextBlock <TextBlock
Grid.Row="6" Grid.Row="6"
@@ -641,7 +664,7 @@
Width="200" Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Watermark="1280" /> PlaceholderText="1280" />
</Grid> </Grid>
<Grid <Grid
x:Name="gridAnytls" x:Name="gridAnytls"
@@ -1,15 +1,12 @@
using System.Reactive.Disposables;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
public partial class AddServerWindow : WindowBase<AddServerViewModel> public partial class AddServerWindow : WindowBase<AddServerViewModel>
{ {
public AddServerWindow() public AddServerWindow()
{
InitializeComponent();
}
public AddServerWindow(ProfileItem profileItem)
{ {
InitializeComponent(); InitializeComponent();
@@ -21,8 +18,6 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
btnGUID.Click += btnGUID_Click; btnGUID.Click += btnGUID_Click;
btnGUID5.Click += btnGUID_Click; btnGUID5.Click += btnGUID_Click;
ViewModel = new AddServerViewModel(profileItem, UpdateViewHandler);
cmbCoreType.ItemsSource = Global.CoreTypes.AppendEmpty(); cmbCoreType.ItemsSource = Global.CoreTypes.AppendEmpty();
cmbNetwork.ItemsSource = Global.Networks; cmbNetwork.ItemsSource = Global.Networks;
@@ -39,8 +34,173 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
cmbFingerprint2.ItemsSource = Global.Fingerprints; cmbFingerprint2.ItemsSource = Global.Fingerprints;
cmbAlpn.ItemsSource = Global.Alpns; cmbAlpn.ItemsSource = Global.Alpns;
var lstStreamSecurity = new List<string> { string.Empty, Global.StreamSecurity }; gridTlsMore.IsVisible = false;
this.WhenActivated(disposables =>
{
var configTypeBindings = new SerialDisposable().DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Address, v => v.txtAddress.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables);
this.WhenAnyValue(v => v.ViewModel.SelectedSource.ConfigType)
.Subscribe(configType =>
{
var currentTypeDisposables = new CompositeDisposable();
configTypeBindings.Disposable = currentTypeDisposables;
switch (configType)
{
case EConfigType.VMess:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.AlterId, v => v.txtAlterId.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.VmessSecurity, v => v.cmbSecurity.SelectedValue).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(currentTypeDisposables);
break;
case EConfigType.Shadowsocks:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId3.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.SsMethod, v => v.cmbSecurity3.SelectedValue).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled3.IsChecked).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(currentTypeDisposables);
break;
case EConfigType.SOCKS:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text)
.DisposeWith(disposables);
break;
case EConfigType.HTTP:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.HttpHeadersJson, v => v.txtHttpHeadersJson.Text)
.DisposeWith(disposables);
break;
case EConfigType.VLESS:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId5.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow5.SelectedValue).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.VlessEncryption, v => v.txtSecurity5.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(currentTypeDisposables);
break;
case EConfigType.Trojan:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId6.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow6.SelectedValue).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(currentTypeDisposables);
break;
case EConfigType.Hysteria2:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId7.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.SalamanderPass, v => v.txtPath7.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Ports, v => v.txtPorts7.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Hy2RealmUrl, v => v.txtHy2RealmUrl.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.GeckoMinPacketSize, v => v.txtMinGeckoPacketSize.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.GeckoMaxPacketSize, v => v.txtMaxGeckoPacketSize.Text).DisposeWith(currentTypeDisposables);
break;
case EConfigType.TUIC:
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId8.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity8.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl8.SelectedValue).DisposeWith(currentTypeDisposables);
break;
case EConfigType.WireGuard:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId9.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.WgPublicKey, v => v.txtPublicKey9.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.WgPresharedKey, v => v.txtPreSharedKey9.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(currentTypeDisposables);
break;
case EConfigType.Anytls:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId11.Text).DisposeWith(currentTypeDisposables);
break;
case EConfigType.Naive:
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId12.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity12.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.togNaiveQuic12.IsChecked).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.cmbCongestionControl12.IsEnabled).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl12.SelectedValue).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(currentTypeDisposables);
break;
}
})
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostRaw.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathRaw.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.KcpHeaderType, v => v.cmbHeaderTypeKcp.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.KcpSeed, v => v.txtKcpSeed.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.KcpMtu, v => v.txtKcpMtu.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostWs.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathWs.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostHttpupgrade.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathHttpupgrade.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.XhttpMode, v => v.cmbHeaderTypeXhttp.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostXhttp.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathXhttp.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.XhttpExtra, v => v.txtExtraXhttp.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GrpcMode, v => v.cmbHeaderTypeGrpc.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GrpcAuthority, v => v.txtRequestHostGrpc.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GrpcServiceName, v => v.txtPathGrpc.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.StreamSecurity, v => v.cmbStreamSecurity.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AllowInsecure, v => v.togAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables);
//reality
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.PublicKey, v => v.txtPublicKey.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.ShortId, v => v.txtShortId.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Finalmask, v => v.txtFinalmask.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
this.WhenAnyValue(v => v.ViewModel.SelectedSource)
.WhereNotNull()
.Subscribe(InitializeData)
.DisposeWith(disposables);
});
}
private void InitializeData(ProfileItem profileItem)
{
Title = $"{profileItem.ConfigType}";
var lstStreamSecurity = new List<string> { string.Empty, Global.StreamSecurity };
switch (profileItem.ConfigType) switch (profileItem.ConfigType)
{ {
case EConfigType.VMess: case EConfigType.VMess:
@@ -54,8 +214,13 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
break; break;
case EConfigType.SOCKS: case EConfigType.SOCKS:
gridSocks.IsVisible = true;
break;
case EConfigType.HTTP: case EConfigType.HTTP:
gridSocks.IsVisible = true; gridSocks.IsVisible = true;
tbHttpHeaders.IsVisible = true;
txtHttpHeadersJson.IsVisible = true;
break; break;
case EConfigType.VLESS: case EConfigType.VLESS:
@@ -121,154 +286,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
break; break;
} }
cmbStreamSecurity.ItemsSource = lstStreamSecurity; cmbStreamSecurity.ItemsSource = lstStreamSecurity;
cmbStreamSecurity.SelectedItem = profileItem.StreamSecurity;
gridTlsMore.IsVisible = false;
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Address, v => v.txtAddress.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables);
switch (profileItem.ConfigType)
{
case EConfigType.VMess:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.VmessSecurity, v => v.cmbSecurity.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables);
break;
case EConfigType.Shadowsocks:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId3.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SsMethod, v => v.cmbSecurity3.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled3.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables);
break;
case EConfigType.SOCKS:
case EConfigType.HTTP:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text).DisposeWith(disposables);
break;
case EConfigType.VLESS:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId5.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow5.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.VlessEncryption, v => v.txtSecurity5.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(disposables);
break;
case EConfigType.Trojan:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId6.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow6.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(disposables);
break;
case EConfigType.Hysteria2:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SalamanderPass, v => v.txtPath7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Ports, v => v.txtPorts7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hy2RealmUrl, v => v.txtHy2RealmUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GeckoMinPacketSize, v => v.txtMinGeckoPacketSize.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GeckoMaxPacketSize, v => v.txtMaxGeckoPacketSize.Text).DisposeWith(disposables);
break;
case EConfigType.TUIC:
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId8.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity8.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl8.SelectedValue).DisposeWith(disposables);
break;
case EConfigType.WireGuard:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.WgPublicKey, v => v.txtPublicKey9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.WgPresharedKey, v => v.txtPreSharedKey9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(disposables);
break;
case EConfigType.Anytls:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId11.Text).DisposeWith(disposables);
break;
case EConfigType.Naive:
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId12.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity12.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.togNaiveQuic12.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.cmbCongestionControl12.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl12.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(disposables);
break;
}
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostRaw.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathRaw.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.KcpHeaderType, v => v.cmbHeaderTypeKcp.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.KcpSeed, v => v.txtKcpSeed.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.KcpMtu, v => v.txtKcpMtu.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostWs.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathWs.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostHttpupgrade.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathHttpupgrade.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.XhttpMode, v => v.cmbHeaderTypeXhttp.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostXhttp.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathXhttp.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.XhttpExtra, v => v.txtExtraXhttp.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GrpcMode, v => v.cmbHeaderTypeGrpc.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GrpcAuthority, v => v.txtRequestHostGrpc.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GrpcServiceName, v => v.txtPathGrpc.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.StreamSecurity, v => v.cmbStreamSecurity.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AllowInsecure, v => v.togAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables);
//reality
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.PublicKey, v => v.txtPublicKey.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.ShortId, v => v.txtShortId.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Finalmask, v => v.txtFinalmask.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
Title = $"{profileItem.ConfigType}";
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
}
return await Task.FromResult(true);
} }
private void Window_Loaded(object? sender, RoutedEventArgs e) private void Window_Loaded(object? sender, RoutedEventArgs e)
@@ -3,10 +3,12 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
Design.DataContext="{x:Static dd:DesignData.BackupAndRestore}"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Styles> <UserControl.Styles>
<Style Selector="Button"> <Style Selector="Button">
@@ -18,8 +20,8 @@
<StackPanel Margin="{StaticResource Margin4}" DockPanel.Dock="Bottom"> <StackPanel Margin="{StaticResource Margin4}" DockPanel.Dock="Bottom">
<TextBlock <TextBlock
Name="txtMsg" Name="txtMsg"
HorizontalAlignment="Left" Margin="{StaticResource Margin4}"
Margin="{StaticResource Margin4}" /> HorizontalAlignment="Left" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
@@ -41,15 +43,15 @@
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.menuLocalBackup}" /> Text="{x:Static resx:ResUI.menuLocalBackup}" />
<Button <Button
Name="menuLocalBackup" Name="menuLocalBackup"
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Content="{x:Static resx:ResUI.menuLocalBackup}" /> Content="{x:Static resx:ResUI.menuLocalBackup}" />
<Separator <Separator
@@ -61,15 +63,15 @@
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.menuLocalRestore}" /> Text="{x:Static resx:ResUI.menuLocalRestore}" />
<Button <Button
Name="menuLocalRestore" Name="menuLocalRestore"
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Content="{x:Static resx:ResUI.menuLocalRestore}" /> Content="{x:Static resx:ResUI.menuLocalRestore}" />
</Grid> </Grid>
</Border> </Border>
@@ -87,11 +89,9 @@
Orientation="Horizontal"> Orientation="Horizontal">
<TextBlock Margin="{StaticResource Margin4}" Text="{x:Static resx:ResUI.menuRemoteBackupAndRestore}" /> <TextBlock Margin="{StaticResource Margin4}" Text="{x:Static resx:ResUI.menuRemoteBackupAndRestore}" />
<Button <Button Margin="{StaticResource MarginLr8}" Classes="IconButton">
Classes="IconButton"
Margin="{StaticResource MarginLr8}">
<Button.Content> <Button.Content>
<PathIcon Data="{StaticResource SemiIconMore}" > <PathIcon Data="{StaticResource SemiIconMore}">
<PathIcon.RenderTransform> <PathIcon.RenderTransform>
<RotateTransform Angle="90" /> <RotateTransform Angle="90" />
</PathIcon.RenderTransform> </PathIcon.RenderTransform>
@@ -104,67 +104,67 @@
<TextBlock <TextBlock
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvWebDavUrl}" /> Text="{x:Static resx:ResUI.LvWebDavUrl}" />
<TextBox <TextBox
x:Name="txtWebDavUrl" x:Name="txtWebDavUrl"
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvWebDavUserName}" /> Text="{x:Static resx:ResUI.LvWebDavUserName}" />
<TextBox <TextBox
x:Name="txtWebDavUserName" x:Name="txtWebDavUserName"
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" Margin="{StaticResource Margin4}"
Margin="{StaticResource Margin4}" /> VerticalAlignment="Center" />
<TextBlock <TextBlock
Grid.Row="2" Grid.Row="2"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvWebDavPassword}" /> Text="{x:Static resx:ResUI.LvWebDavPassword}" />
<TextBox <TextBox
x:Name="txtWebDavPassword" x:Name="txtWebDavPassword"
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" Margin="{StaticResource Margin4}"
Margin="{StaticResource Margin4}" /> VerticalAlignment="Center" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvWebDavDirName}" /> Text="{x:Static resx:ResUI.LvWebDavDirName}" />
<TextBox <TextBox
x:Name="txtWebDavDirName" x:Name="txtWebDavDirName"
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" Margin="{StaticResource Margin4}"
Margin="{StaticResource Margin4}" /> VerticalAlignment="Center" />
<Button <Button
x:Name="menuWebDavCheck" x:Name="menuWebDavCheck"
Grid.Row="4" Grid.Row="4"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.LvWebDavCheck}" /> Content="{x:Static resx:ResUI.LvWebDavCheck}" />
</Grid> </Grid>
</StackPanel> </StackPanel>
@@ -177,15 +177,15 @@
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.menuRemoteBackup}" /> Text="{x:Static resx:ResUI.menuRemoteBackup}" />
<Button <Button
Name="menuRemoteBackup" Name="menuRemoteBackup"
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Content="{x:Static resx:ResUI.menuRemoteBackup}" /> Content="{x:Static resx:ResUI.menuRemoteBackup}" />
<Separator <Separator
@@ -196,15 +196,15 @@
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.menuRemoteRestore}" /> Text="{x:Static resx:ResUI.menuRemoteRestore}" />
<Button <Button
Name="menuRemoteRestore" Name="menuRemoteRestore"
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Content="{x:Static resx:ResUI.menuRemoteRestore}" /> Content="{x:Static resx:ResUI.menuRemoteRestore}" />
</Grid> </Grid>
</Border> </Border>
@@ -4,23 +4,12 @@ namespace v2rayN.Desktop.Views;
public partial class BackupAndRestoreView : ReactiveUserControl<BackupAndRestoreViewModel> public partial class BackupAndRestoreView : ReactiveUserControl<BackupAndRestoreViewModel>
{ {
private Window? _window;
public BackupAndRestoreView() public BackupAndRestoreView()
{ {
InitializeComponent(); InitializeComponent();
}
public BackupAndRestoreView(Window window)
{
_window = window;
InitializeComponent();
menuLocalBackup.Click += MenuLocalBackup_Click; menuLocalBackup.Click += MenuLocalBackup_Click;
menuLocalRestore.Click += MenuLocalRestore_Click; menuLocalRestore.Click += MenuLocalRestore_Click;
ViewModel = new BackupAndRestoreViewModel(UpdateViewHandler);
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.Bind(ViewModel, vm => vm.OperationMsg, v => v.txtMsg.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.OperationMsg, v => v.txtMsg.Text).DisposeWith(disposables);
@@ -39,7 +28,7 @@ public partial class BackupAndRestoreView : ReactiveUserControl<BackupAndRestore
private async void MenuLocalBackup_Click(object? sender, RoutedEventArgs e) private async void MenuLocalBackup_Click(object? sender, RoutedEventArgs e)
{ {
var fileName = await UI.SaveFileDialog(_window, "Zip|*.zip"); var fileName = await UI.SaveFileDialog("Zip|*.zip");
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
{ {
return; return;
@@ -50,7 +39,7 @@ public partial class BackupAndRestoreView : ReactiveUserControl<BackupAndRestore
private async void MenuLocalRestore_Click(object? sender, RoutedEventArgs e) private async void MenuLocalRestore_Click(object? sender, RoutedEventArgs e)
{ {
var fileName = await UI.OpenFileDialog(_window, null); var fileName = await UI.OpenFileDialog(null);
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
{ {
return; return;
@@ -58,9 +47,4 @@ public partial class BackupAndRestoreView : ReactiveUserControl<BackupAndRestore
ViewModel?.LocalRestore(fileName); ViewModel?.LocalRestore(fileName);
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
return await Task.FromResult(true);
}
} }
@@ -3,12 +3,14 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
d:DesignHeight="600" d:DesignHeight="600"
d:DesignWidth="800" d:DesignWidth="800"
x:DataType="vms:CheckUpdateViewModel" x:DataType="vms:CheckUpdateViewModel"
Design.DataContext="{x:Static dd:DesignData.CheckUpdate}"
mc:Ignorable="d"> mc:Ignorable="d">
<DockPanel Margin="{StaticResource Margin8}"> <DockPanel Margin="{StaticResource Margin8}">
@@ -6,8 +6,6 @@ public partial class CheckUpdateView : ReactiveUserControl<CheckUpdateViewModel>
{ {
InitializeComponent(); InitializeComponent();
ViewModel = new CheckUpdateViewModel(UpdateViewHandler);
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.OneWayBind(ViewModel, vm => vm.CheckUpdateModels, v => v.lstCheckUpdates.ItemsSource).DisposeWith(disposables); this.OneWayBind(ViewModel, vm => vm.CheckUpdateModels, v => v.lstCheckUpdates.ItemsSource).DisposeWith(disposables);
@@ -17,9 +15,4 @@ public partial class CheckUpdateView : ReactiveUserControl<CheckUpdateViewModel>
this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables);
}); });
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
return await Task.FromResult(true);
}
} }
@@ -3,32 +3,34 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
x:DataType="vms:ClashConnectionsViewModel" x:DataType="vms:ClashConnectionsViewModel"
Design.DataContext="{x:Static dd:DesignData.ClashConnections}"
mc:Ignorable="d"> mc:Ignorable="d">
<DockPanel Margin="2"> <DockPanel Margin="2">
<WrapPanel <WrapPanel
VerticalAlignment="Center"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
DockPanel.Dock="Top" DockPanel.Dock="Top"
Orientation="Horizontal"> Orientation="Horizontal">
<TextBox <TextBox
x:Name="txtHostFilter" x:Name="txtHostFilter"
Width="200" Width="200"
VerticalContentAlignment="Center"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Watermark="{x:Static resx:ResUI.ConnectionsHostFilterTitle}" /> VerticalContentAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.ConnectionsHostFilterTitle}" />
<Button <Button
x:Name="btnConnectionCloseAll" x:Name="btnConnectionCloseAll"
Classes="IconButton Success"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Classes="IconButton Success"
ToolTip.Tip="{x:Static resx:ResUI.menuConnectionCloseAll}"> ToolTip.Tip="{x:Static resx:ResUI.menuConnectionCloseAll}">
<Button.Content> <Button.Content>
<PathIcon Data="{StaticResource SemiIconClose}" /> <PathIcon Data="{StaticResource SemiIconClose}" />
@@ -37,8 +39,8 @@
<Button <Button
x:Name="btnAutofitColumnWidth" x:Name="btnAutofitColumnWidth"
Classes="IconButton Success"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Classes="IconButton Success"
ToolTip.Tip="{x:Static resx:ResUI.menuProfileAutofitColumnWidth}"> ToolTip.Tip="{x:Static resx:ResUI.menuProfileAutofitColumnWidth}">
<Button.Content> <Button.Content>
<PathIcon Data="{StaticResource SemiIconExpand}" /> <PathIcon Data="{StaticResource SemiIconExpand}" />
@@ -46,13 +48,13 @@
</Button> </Button>
<TextBlock <TextBlock
VerticalAlignment="Center"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbAutoRefresh}" /> Text="{x:Static resx:ResUI.TbAutoRefresh}" />
<ToggleSwitch <ToggleSwitch
x:Name="togAutoRefresh" x:Name="togAutoRefresh"
HorizontalAlignment="Left" Margin="{StaticResource MarginLr8}"
Margin="{StaticResource MarginLr8}" /> HorizontalAlignment="Left" />
</WrapPanel> </WrapPanel>
<DataGrid <DataGrid
@@ -11,7 +11,6 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
ViewModel = new ClashConnectionsViewModel(UpdateViewHandler);
btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click;
this.WhenActivated(disposables => this.WhenActivated(disposables =>
@@ -36,11 +35,6 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
RestoreUI(); RestoreUI();
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
return await Task.FromResult(true);
}
private void BtnAutofitColumnWidth_Click(object? sender, RoutedEventArgs e) private void BtnAutofitColumnWidth_Click(object? sender, RoutedEventArgs e)
{ {
AutofitColumnWidth(); AutofitColumnWidth();
@@ -4,12 +4,14 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
x:DataType="vms:ClashProxiesViewModel" x:DataType="vms:ClashProxiesViewModel"
Design.DataContext="{x:Static dd:DesignData.ClashProxies}"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<conv:DelayColorConverter x:Key="DelayColorConverter" /> <conv:DelayColorConverter x:Key="DelayColorConverter" />
@@ -5,7 +5,6 @@ public partial class ClashProxiesView : ReactiveUserControl<ClashProxiesViewMode
public ClashProxiesView() public ClashProxiesView()
{ {
InitializeComponent(); InitializeComponent();
ViewModel = new ClashProxiesViewModel(UpdateViewHandler);
lstProxyDetails.DoubleTapped += LstProxyDetails_DoubleTapped; lstProxyDetails.DoubleTapped += LstProxyDetails_DoubleTapped;
KeyDown += ClashProxiesView_KeyDown; KeyDown += ClashProxiesView_KeyDown;
@@ -29,11 +28,6 @@ public partial class ClashProxiesView : ReactiveUserControl<ClashProxiesViewMode
}); });
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
return await Task.FromResult(true);
}
private void ClashProxiesView_KeyDown(object? sender, KeyEventArgs e) private void ClashProxiesView_KeyDown(object? sender, KeyEventArgs e)
{ {
switch (e.Key) switch (e.Key)
@@ -3,6 +3,7 @@
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: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:local="using:v2rayN.Desktop.Views" xmlns:local="using:v2rayN.Desktop.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@@ -11,6 +12,7 @@
Width="900" Width="900"
Height="600" Height="600"
x:DataType="vms:DNSSettingViewModel" x:DataType="vms:DNSSettingViewModel"
Design.DataContext="{x:Static dd:DesignData.DNSSetting}"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -317,7 +319,7 @@
BorderThickness="1" BorderThickness="1"
Classes="TextArea" Classes="TextArea"
TextWrapping="Wrap" TextWrapping="Wrap"
Watermark="{x:Static resx:ResUI.TbDNSHostsConfig}" /> PlaceholderText="{x:Static resx:ResUI.TbDNSHostsConfig}" />
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
@@ -13,7 +13,6 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
Loaded += Window_Loaded; Loaded += Window_Loaded;
btnCancel.Click += (s, e) => Close(); btnCancel.Click += (s, e) => Close();
ViewModel = new DNSSettingViewModel(UpdateViewHandler);
cmbDirectDNSStrategy.ItemsSource = Global.DomainStrategy; cmbDirectDNSStrategy.ItemsSource = Global.DomainStrategy;
cmbRemoteDNSStrategy.ItemsSource = Global.DomainStrategy; cmbRemoteDNSStrategy.ItemsSource = Global.DomainStrategy;
@@ -64,26 +63,15 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled) this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
.Select(b => !b) .Select(b => !b)
.BindTo(this.FindControl<TextBlock>("txtBasicDNSSettingsInvalid"), t => t.IsVisible); .BindTo(txtBasicDNSSettingsInvalid, t => t.IsVisible);
this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled) this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
.Select(b => !b) .Select(b => !b)
.BindTo(this.FindControl<TextBlock>("txtAdvancedDNSSettingsInvalid"), t => t.IsVisible); .BindTo(txtAdvancedDNSSettingsInvalid, t => t.IsVisible);
this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridBasicDNSSettings.IsEnabled).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridBasicDNSSettings.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridAdvancedDNSSettings.IsEnabled).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridAdvancedDNSSettings.IsEnabled).DisposeWith(disposables);
}); });
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
}
return await Task.FromResult(true);
}
private void linkDnsObjectDoc_Click(object? sender, RoutedEventArgs e) private void linkDnsObjectDoc_Click(object? sender, RoutedEventArgs e)
{ {
ProcUtils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject"); ProcUtils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject");
@@ -3,6 +3,7 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:views="clr-namespace:v2rayN.Desktop.Views" xmlns:views="clr-namespace:v2rayN.Desktop.Views"
@@ -11,6 +12,7 @@
Width="900" Width="900"
Height="600" Height="600"
x:DataType="vms:FullConfigTemplateViewModel" x:DataType="vms:FullConfigTemplateViewModel"
Design.DataContext="{x:Static dd:DesignData.FullConfigTemplate}"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -13,7 +13,6 @@ public partial class FullConfigTemplateWindow : WindowBase<FullConfigTemplateVie
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
Loaded += Window_Loaded; Loaded += Window_Loaded;
btnCancel.Click += (_, _) => Close(); btnCancel.Click += (_, _) => Close();
ViewModel = new FullConfigTemplateViewModel(UpdateViewHandler);
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
@@ -32,17 +31,6 @@ public partial class FullConfigTemplateWindow : WindowBase<FullConfigTemplateVie
}); });
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
}
return await Task.FromResult(true);
}
private void linkFullConfigTemplateDoc_Click(object sender, RoutedEventArgs e) private void linkFullConfigTemplateDoc_Click(object sender, RoutedEventArgs e)
{ {
ProcUtils.ProcessStart("https://github.com/2dust/v2rayN/wiki/Description-of-some-ui#%E5%AE%8C%E6%95%B4%E9%85%8D%E7%BD%AE%E6%A8%A1%E6%9D%BF%E8%AE%BE%E7%BD%AE"); ProcUtils.ProcessStart("https://github.com/2dust/v2rayN/wiki/Description-of-some-ui#%E5%AE%8C%E6%95%B4%E9%85%8D%E7%BD%AE%E6%A8%A1%E6%9D%BF%E8%AE%BE%E7%BD%AE");
@@ -3,6 +3,7 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
@@ -10,6 +11,7 @@
Width="700" Width="700"
Height="500" Height="500"
x:DataType="vms:GlobalHotkeySettingViewModel" x:DataType="vms:GlobalHotkeySettingViewModel"
Design.DataContext="{x:Static dd:DesignData.GlobalHotkeySetting}"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -11,8 +11,6 @@ public partial class GlobalHotkeySettingWindow : WindowBase<GlobalHotkeySettingV
{ {
InitializeComponent(); InitializeComponent();
ViewModel = new GlobalHotkeySettingViewModel(UpdateViewHandler);
btnReset.Click += btnReset_Click; btnReset.Click += btnReset_Click;
HotkeyManager.Instance.IsPause = true; HotkeyManager.Instance.IsPause = true;
@@ -23,21 +21,10 @@ public partial class GlobalHotkeySettingWindow : WindowBase<GlobalHotkeySettingV
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
BindingData();
}); });
Init(); Init();
BindingData();
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
}
return await Task.FromResult(true);
} }
private void Init() private void Init()
@@ -67,9 +54,13 @@ public partial class GlobalHotkeySettingWindow : WindowBase<GlobalHotkeySettingV
{ {
return; return;
} }
if (ViewModel == null)
{
return;
}
var item = ViewModel?.GetKeyEventItem((EGlobalHotkey)txtBox.Tag); var item = ViewModel.GetKeyEventItem((EGlobalHotkey)txtBox.Tag!);
var modifierKeys = new Key[] { Key.LeftCtrl, Key.RightCtrl, Key.LeftShift, Key.RightShift, Key.LeftAlt, Key.RightAlt, Key.LWin, Key.RWin }; var modifierKeys = new[] { Key.LeftCtrl, Key.RightCtrl, Key.LeftShift, Key.RightShift, Key.LeftAlt, Key.RightAlt, Key.LWin, Key.RWin };
item.KeyCode = (int)(e.Key == Key.System ? modifierKeys.Contains(Key.System) ? Key.None : Key.System : modifierKeys.Contains(e.Key) ? Key.None : e.Key); item.KeyCode = (int)(e.Key == Key.System ? modifierKeys.Contains(Key.System) ? Key.None : Key.System : modifierKeys.Contains(e.Key) ? Key.None : e.Key);
item.Alt = (e.KeyModifiers & KeyModifiers.Alt) == KeyModifiers.Alt; item.Alt = (e.KeyModifiers & KeyModifiers.Alt) == KeyModifiers.Alt;
@@ -109,17 +100,17 @@ public partial class GlobalHotkeySettingWindow : WindowBase<GlobalHotkeySettingV
if (item.Control) if (item.Control)
{ {
res.Append($"{KeyModifiers.Control} +"); res.Append($"{KeyModifiers.Control} + ");
} }
if (item.Shift) if (item.Shift)
{ {
res.Append($"{KeyModifiers.Shift} +"); res.Append($"{KeyModifiers.Shift} + ");
} }
if (item.Alt) if (item.Alt)
{ {
res.Append($"{KeyModifiers.Alt} +"); res.Append($"{KeyModifiers.Alt} + ");
} }
if (item.KeyCode != null && (Key)item.KeyCode != Key.None) if (item.KeyCode != null && (Key)item.KeyCode != Key.None)
+4 -1
View File
@@ -3,7 +3,10 @@
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:ae="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" xmlns:ae="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"> xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
d:DesignHeight="450"
d:DesignWidth="800">
<ae:TextEditor <ae:TextEditor
Name="Editor" Name="Editor"
@@ -27,6 +27,7 @@ public partial class JsonEditor : UserControl
public JsonEditor() public JsonEditor()
{ {
InitializeComponent(); InitializeComponent();
var isDark = Application.Current?.ActualThemeVariant != ThemeVariant.Light; var isDark = Application.Current?.ActualThemeVariant != ThemeVariant.Light;
Editor.SyntaxHighlighting = isDark ? SHighlightingDark.Value : SHighlightingLight.Value; Editor.SyntaxHighlighting = isDark ? SHighlightingDark.Value : SHighlightingLight.Value;
Editor.TextArea.TextView.Options.EnableHyperlinks = false; Editor.TextArea.TextView.Options.EnableHyperlinks = false;
@@ -46,6 +47,11 @@ public partial class JsonEditor : UserControl
Editor.Text = text ?? string.Empty; Editor.Text = text ?? string.Empty;
} }
}); });
if (Design.IsDesignMode)
{
Text = "{\n \"key\": \"value\",\n \"number\": 123,\n \"boolean\": true,\n \"nullValue\": null\n}";
}
} }
private static IHighlightingDefinition BuildHighlighting(bool dark) private static IHighlightingDefinition BuildHighlighting(bool dark)
+3 -2
View File
@@ -3,16 +3,17 @@
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: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:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia" xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:view="using:v2rayN.Desktop.Views"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Title="v2rayN" Title="v2rayN"
Width="1200" Width="1200"
Height="800" Height="800"
MinWidth="600" MinWidth="600"
x:DataType="vms:MainWindowViewModel" x:DataType="vms:MainWindowViewModel"
Design.DataContext="{x:Static dd:DesignData.MainWindow}"
Icon="/Assets/NotifyIcon1.ico" Icon="/Assets/NotifyIcon1.ico"
ShowInTaskbar="True" ShowInTaskbar="True"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
@@ -111,7 +112,7 @@
IsVisible="False" /> IsVisible="False" />
</DockPanel> </DockPanel>
<view:StatusBarView DockPanel.Dock="Bottom" /> <ContentControl x:Name="contentStatusBarView" DockPanel.Dock="Bottom" />
<Grid> <Grid>
<Grid <Grid
+105 -142
View File
@@ -1,3 +1,4 @@
using System.Reactive.Disposables;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using DialogHostAvalonia; using DialogHostAvalonia;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
@@ -9,6 +10,7 @@ namespace v2rayN.Desktop.Views;
public partial class MainWindow : WindowBase<MainWindowViewModel> public partial class MainWindow : WindowBase<MainWindowViewModel>
{ {
private static Config _config; private static Config _config;
private readonly SerialDisposable _layoutBindingsDisposable = new();
private readonly WindowNotificationManager? _manager; private readonly WindowNotificationManager? _manager;
private CheckUpdateView? _checkUpdateView; private CheckUpdateView? _checkUpdateView;
private BackupAndRestoreView? _backupAndRestoreView; private BackupAndRestoreView? _backupAndRestoreView;
@@ -29,35 +31,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
menuBackupAndRestore.Click += MenuBackupAndRestore_Click; menuBackupAndRestore.Click += MenuBackupAndRestore_Click;
menuClose.Click += MenuClose_Click; menuClose.Click += MenuClose_Click;
ViewModel = new MainWindowViewModel(UpdateViewHandler);
switch (_config.UiItem.MainGirdOrientation)
{
case EGirdOrientation.Horizontal:
tabProfiles.Content ??= new ProfilesView(this);
tabMsgView.Content ??= new MsgView();
tabClashProxies.Content ??= new ClashProxiesView();
tabClashConnections.Content ??= new ClashConnectionsView();
gridMain.IsVisible = true;
break;
case EGirdOrientation.Vertical:
tabProfiles1.Content ??= new ProfilesView(this);
tabMsgView1.Content ??= new MsgView();
tabClashProxies1.Content ??= new ClashProxiesView();
tabClashConnections1.Content ??= new ClashConnectionsView();
gridMain1.IsVisible = true;
break;
case EGirdOrientation.Tab:
default:
tabProfiles2.Content ??= new ProfilesView(this);
tabMsgView2.Content ??= new MsgView();
tabClashProxies2.Content ??= new ClashProxiesView();
tabClashConnections2.Content ??= new ClashConnectionsView();
gridMain2.IsVisible = true;
break;
}
conTheme.Content ??= new ThemeSettingView(); conTheme.Content ??= new ThemeSettingView();
this.WhenActivated(disposables => this.WhenActivated(disposables =>
@@ -105,29 +78,41 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.OneWayBind(ViewModel, vm => vm.BlReloadEnabled, v => v.menuReload.IsEnabled).DisposeWith(disposables); this.OneWayBind(ViewModel, vm => vm.BlReloadEnabled, v => v.menuReload.IsEnabled).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.BlNewUpdate, v => v.btnNewUpdate.IsVisible).DisposeWith(disposables); this.OneWayBind(ViewModel, vm => vm.BlNewUpdate, v => v.btnNewUpdate.IsVisible).DisposeWith(disposables);
switch (_config.UiItem.MainGirdOrientation) this.OneWayBind(ViewModel, vm => vm.StatusBarViewModel, v => v.contentStatusBarView.Content).DisposeWith(disposables);
_layoutBindingsDisposable.DisposeWith(disposables);
this.WhenAnyValue(v => v.ViewModel.MainGirdOrientation)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(UpdateLayout)
.DisposeWith(disposables);
ViewModel.ReadTextFromClipboardInteraction.RegisterHandler(async interaction =>
{ {
case EGirdOrientation.Horizontal: var result = await AvaUtils.GetClipboardData(this);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView.IsVisible).DisposeWith(disposables); interaction.SetOutput(result);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies.IsVisible).DisposeWith(disposables); }).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections.IsVisible).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain.SelectedIndex).DisposeWith(disposables);
break;
case EGirdOrientation.Vertical: ViewModel.ScanScreenInteraction.RegisterHandler(async interaction =>
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView1.IsVisible).DisposeWith(disposables); {
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies1.IsVisible).DisposeWith(disposables); ShowHideWindow(false);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections1.IsVisible).DisposeWith(disposables); await Task.Delay(200);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain1.SelectedIndex).DisposeWith(disposables); var result = QRCodeAvaloniaUtils.CaptureScreen();
break; ShowHideWindow(true);
interaction.SetOutput(result);
}).DisposeWith(disposables);
case EGirdOrientation.Tab: ViewModel.BrowseImageFileInteraction.RegisterHandler(async interaction =>
default: {
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies2.IsVisible).DisposeWith(disposables); var result = await UI.OpenFileDialog(null);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections2.IsVisible).DisposeWith(disposables); interaction.SetOutput(result);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain2.SelectedIndex).DisposeWith(disposables); }).DisposeWith(disposables);
break;
} ViewModel.ShowHideWindowInteraction.RegisterHandler(interaction =>
{
ShowHideWindow(interaction.Input);
interaction.SetOutput(Unit.Default);
}).DisposeWith(disposables);
AppEvents.SendSnackMsgRequested AppEvents.SendSnackMsgRequested
.AsObservable() .AsObservable()
@@ -146,20 +131,17 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(Shutdown) .Subscribe(Shutdown)
.DisposeWith(disposables); .DisposeWith(disposables);
AppEvents.ShowHideWindowRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(ShowHideWindow)
.DisposeWith(disposables);
}); });
if (Utils.IsWindows()) if (Utils.IsWindows())
{ {
Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}"; Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}";
ThreadPool.RegisterWaitForSingleObject(Program.ProgramStarted, OnProgramStarted, null, -1, false); if (!Design.IsDesignMode)
HotkeyManager.Instance.Init(_config, OnHotkeyHandler); {
ThreadPool.RegisterWaitForSingleObject(Program.ProgramStarted, OnProgramStarted, null, -1, false);
HotkeyManager.Instance.Init(_config, OnHotkeyHandler);
}
} }
else else
{ {
@@ -186,72 +168,10 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
private async Task DelegateSnackMsg(string content) private async Task DelegateSnackMsg(string content)
{ {
_manager?.Show(new Notification(null, content, NotificationType.Information)); _manager?.Show(new Avalonia.Controls.Notifications.Notification(null, content, NotificationType.Information));
await Task.CompletedTask; await Task.CompletedTask;
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.AddServerWindow:
if (obj is null)
{
return false;
}
return await new AddServerWindow((ProfileItem)obj).ShowDialog<bool>(this);
case EViewAction.AddServer2Window:
if (obj is null)
{
return false;
}
return await new AddServer2Window((ProfileItem)obj).ShowDialog<bool>(this);
case EViewAction.AddGroupServerWindow:
if (obj is null)
{
return false;
}
return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog<bool>(this);
case EViewAction.DNSSettingWindow:
return await new DNSSettingWindow().ShowDialog<bool>(this);
case EViewAction.FullConfigTemplateWindow:
return await new FullConfigTemplateWindow().ShowDialog<bool>(this);
case EViewAction.RoutingSettingWindow:
return await new RoutingSettingWindow().ShowDialog<bool>(this);
case EViewAction.OptionSettingWindow:
return await new OptionSettingWindow().ShowDialog<bool>(this);
case EViewAction.GlobalHotkeySettingWindow:
return await new GlobalHotkeySettingWindow().ShowDialog<bool>(this);
case EViewAction.SubSettingWindow:
return await new SubSettingWindow().ShowDialog<bool>(this);
case EViewAction.ScanScreenTask:
await ScanScreenTaskAsync();
break;
case EViewAction.ScanImageTask:
await ScanImageTaskAsync();
break;
case EViewAction.AddServerViaClipboard:
await AddServerViaClipboardAsync();
break;
}
return await Task.FromResult(true);
}
private void OnHotkeyHandler(EGlobalHotkey e) private void OnHotkeyHandler(EGlobalHotkey e)
{ {
switch (e) switch (e)
@@ -351,23 +271,10 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
ShowHideWindow(true); ShowHideWindow(true);
} }
private async Task ScanImageTaskAsync()
{
var fileName = await UI.OpenFileDialog(this, null);
if (fileName.IsNullOrEmpty())
{
return;
}
if (ViewModel != null)
{
await ViewModel.ScanImageResult(fileName);
}
}
private void MenuCheckUpdate_Click(object? sender, RoutedEventArgs e) private void MenuCheckUpdate_Click(object? sender, RoutedEventArgs e)
{ {
_checkUpdateView ??= new CheckUpdateView(); _checkUpdateView ??= new CheckUpdateView();
_checkUpdateView.ViewModel = ViewModel?.CheckUpdateViewModel;
DialogHost.Show(_checkUpdateView); DialogHost.Show(_checkUpdateView);
AppEvents.HasUpdateNotified.Publish(false); AppEvents.HasUpdateNotified.Publish(false);
@@ -375,21 +282,29 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
private void MenuBackupAndRestore_Click(object? sender, RoutedEventArgs e) private void MenuBackupAndRestore_Click(object? sender, RoutedEventArgs e)
{ {
_backupAndRestoreView ??= new BackupAndRestoreView(this); _backupAndRestoreView ??= new BackupAndRestoreView();
_backupAndRestoreView.ViewModel = ViewModel?.BackupAndRestoreViewModel;
DialogHost.Show(_backupAndRestoreView); DialogHost.Show(_backupAndRestoreView);
} }
private async void MenuClose_Click(object? sender, RoutedEventArgs e) private async void MenuClose_Click(object? sender, RoutedEventArgs e)
{ {
if (await UI.ShowYesNo(this, ResUI.menuExitTips) != ButtonResult.Yes) try
{ {
return; if (await UI.ShowYesNo(ResUI.menuExitTips) != ButtonResult.Yes)
{
return;
}
_blCloseByUser = true;
StorageUI();
await AppManager.Instance.AppExitAsync(true);
}
catch
{
// Ignore
} }
_blCloseByUser = true;
StorageUI();
await AppManager.Instance.AppExitAsync(true);
} }
private void Shutdown(bool obj) private void Shutdown(bool obj)
@@ -485,6 +400,54 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
} }
} }
private void UpdateLayout(EGirdOrientation orientation)
{
var currentLayoutDisposables = new CompositeDisposable();
_layoutBindingsDisposable.Disposable = currentLayoutDisposables;
gridMain.IsVisible = orientation == EGirdOrientation.Horizontal;
gridMain1.IsVisible = orientation == EGirdOrientation.Vertical;
gridMain2.IsVisible = orientation == EGirdOrientation.Tab;
switch (orientation)
{
case EGirdOrientation.Horizontal:
this.OneWayBind(ViewModel, vm => vm.ProfilesViewModel, v => v.tabProfiles.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.MsgViewModel, v => v.tabMsgView.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashProxiesViewModel, v => v.tabClashProxies.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashConnectionsViewModel, v => v.tabClashConnections.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections.IsVisible).DisposeWith(currentLayoutDisposables);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain.SelectedIndex).DisposeWith(currentLayoutDisposables);
break;
case EGirdOrientation.Vertical:
this.OneWayBind(ViewModel, vm => vm.ProfilesViewModel, v => v.tabProfiles1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.MsgViewModel, v => v.tabMsgView1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashProxiesViewModel, v => v.tabClashProxies1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashConnectionsViewModel, v => v.tabClashConnections1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView1.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies1.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections1.IsVisible).DisposeWith(currentLayoutDisposables);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain1.SelectedIndex).DisposeWith(currentLayoutDisposables);
break;
case EGirdOrientation.Tab:
default:
this.OneWayBind(ViewModel, vm => vm.ProfilesViewModel, v => v.tabProfiles2.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.MsgViewModel, v => v.tabMsgView2.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashProxiesViewModel, v => v.tabClashProxies2.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashConnectionsViewModel, v => v.tabClashConnections2.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies2.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections2.IsVisible).DisposeWith(currentLayoutDisposables);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain2.SelectedIndex).DisposeWith(currentLayoutDisposables);
break;
}
RestoreUI();
}
private void AddHelpMenuItem() private void AddHelpMenuItem()
{ {
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(); var coreInfo = CoreInfoManager.Instance.GetCoreInfo();
@@ -11,7 +11,7 @@
CanResize="False" CanResize="False"
ShowInTaskbar="False" ShowInTaskbar="False"
SizeToContent="Height" SizeToContent="Height"
SystemDecorations="Full" WindowDecorations="Full"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -13,6 +13,12 @@ public partial class MessageBoxDialog : Window
{ {
InitializeComponent(); InitializeComponent();
if (Design.IsDesignMode)
{
caption = "Design Caption";
message = "Design Message";
}
Title = caption; Title = caption;
txtMessage.Text = message; txtMessage.Text = message;
+3 -1
View File
@@ -4,10 +4,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
Design.DataContext="{x:Static dd:DesignData.Msg}"
mc:Ignorable="d"> mc:Ignorable="d">
<DockPanel Margin="2"> <DockPanel Margin="2">
<WrapPanel <WrapPanel
@@ -21,7 +23,7 @@
Width="200" Width="200"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
Watermark="{x:Static resx:ResUI.MsgFilterTitle}" /> PlaceholderText="{x:Static resx:ResUI.MsgFilterTitle}" />
<Button <Button
x:Name="btnCopy" x:Name="btnCopy"
+8 -18
View File
@@ -10,12 +10,19 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
{ {
InitializeComponent(); InitializeComponent();
txtMsg.TextArea.TextView.Options.EnableHyperlinks = false; txtMsg.TextArea.TextView.Options.EnableHyperlinks = false;
ViewModel = new MsgViewModel(UpdateViewHandler);
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction =>
{
var msg = interaction.Input;
Dispatcher.UIThread.Post(() => ShowMsg(msg),
DispatcherPriority.ApplicationIdle);
interaction.SetOutput(Unit.Default);
}).DisposeWith(disposables);
}); });
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary( TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
@@ -24,23 +31,6 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
)); ));
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.DispatcherShowMsg:
if (obj is null)
{
return false;
}
Dispatcher.UIThread.Post(() => ShowMsg(obj),
DispatcherPriority.ApplicationIdle);
break;
}
return await Task.FromResult(true);
}
private void ShowMsg(object msg) private void ShowMsg(object msg)
{ {
//var lineCount = txtMsg.LineCount; //var lineCount = txtMsg.LineCount;
@@ -3,6 +3,7 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
@@ -10,6 +11,7 @@
Width="1000" Width="1000"
Height="600" Height="600"
x:DataType="vms:OptionSettingViewModel" x:DataType="vms:OptionSettingViewModel"
Design.DataContext="{x:Static dd:DesignData.OptionSetting}"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -279,12 +281,12 @@
x:Name="txtUpMbps" x:Name="txtUpMbps"
Width="90" Width="90"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="Up" /> PlaceholderText="Up" />
<TextBox <TextBox
x:Name="txtDownMbps" x:Name="txtDownMbps"
Width="90" Width="90"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="Down" /> PlaceholderText="Down" />
</StackPanel> </StackPanel>
<TextBlock <TextBlock
@@ -351,7 +353,7 @@
Width="120" Width="120"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Watermark="50-100" /> PlaceholderText="50-100" />
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="1"
Grid.Column="2" Grid.Column="2"
@@ -373,7 +375,7 @@
Width="120" Width="120"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Watermark="10-20" /> PlaceholderText="10-20" />
<TextBlock <TextBlock
Grid.Row="2" Grid.Row="2"
Grid.Column="2" Grid.Column="2"
@@ -395,7 +397,7 @@
Width="120" Width="120"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Watermark="0" /> PlaceholderText="0" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="2" Grid.Column="2"
@@ -461,7 +463,7 @@
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="0.0.0.0" /> PlaceholderText="0.0.0.0" />
<TextBlock <TextBlock
Grid.Row="23" Grid.Row="23"
Grid.Column="2" Grid.Column="2"
@@ -609,9 +611,9 @@
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsDoubleClick2Activate}" /> Text="{x:Static resx:ResUI.TbSettingsEnableDragDropSort}" />
<ToggleSwitch <ToggleSwitch
x:Name="togDoubleClick2Activate" x:Name="togEnableDragDropSort"
Grid.Row="9" Grid.Row="9"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -622,15 +624,28 @@
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsDoubleClick2Activate}" />
<ToggleSwitch
x:Name="togDoubleClick2Activate"
Grid.Row="10"
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="11"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRootCertificateProvider}" /> Text="{x:Static resx:ResUI.TbRootCertificateProvider}" />
<ComboBox <ComboBox
x:Name="cmbRootCertificateProvider" x:Name="cmbRootCertificateProvider"
Grid.Row="10" Grid.Row="11"
Grid.Column="1" Grid.Column="1"
Width="300" Width="300"
Margin="{StaticResource Margin4}" /> Margin="{StaticResource Margin4}" />
<TextBlock <TextBlock
Grid.Row="10" Grid.Row="11"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -864,8 +879,8 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" PlaceholderText="proxy_set.sh"
Watermark="proxy_set.sh" /> TextWrapping="Wrap" />
<Button <Button
x:Name="btnBrowseCustomSystemProxyScriptPath" x:Name="btnBrowseCustomSystemProxyScriptPath"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -909,8 +924,8 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" PlaceholderText="pac.txt"
Watermark="pac.txt" /> TextWrapping="Wrap" />
<Button <Button
x:Name="btnBrowseCustomSystemProxyPacPath" x:Name="btnBrowseCustomSystemProxyPacPath"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -1099,7 +1114,7 @@
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Custom" /> Text="Custom Pre" />
<ComboBox <ComboBox
x:Name="cmbCoreType2" x:Name="cmbCoreType2"
Grid.Row="2" Grid.Row="2"
@@ -15,8 +15,6 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
btnCancel.Click += (s, e) => Close(); btnCancel.Click += (s, e) => Close();
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
ViewModel = new OptionSettingViewModel(UpdateViewHandler);
clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged; clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged;
btnBrowseCustomSystemProxyPacPath.Click += BtnBrowseCustomSystemProxyPacPath_Click; btnBrowseCustomSystemProxyPacPath.Click += BtnBrowseCustomSystemProxyPacPath_Click;
btnBrowseCustomSystemProxyScriptPath.Click += BtnBrowseCustomSystemProxyScriptPath_Click; btnBrowseCustomSystemProxyScriptPath.Click += BtnBrowseCustomSystemProxyScriptPath_Click;
@@ -60,6 +58,8 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
cmbMainGirdOrientation.ItemsSource = Utils.GetEnumNames<EGirdOrientation>(); cmbMainGirdOrientation.ItemsSource = Utils.GetEnumNames<EGirdOrientation>();
_ = InitSettingFont();
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.Bind(ViewModel, vm => vm.LocalPort, v => v.txtlocalPort.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.LocalPort, v => v.txtlocalPort.Text).DisposeWith(disposables);
@@ -97,6 +97,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hide2TrayWhenClose, v => v.togHide2TrayWhenClose.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Hide2TrayWhenClose, v => v.togHide2TrayWhenClose.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MacOSShowInDock, v => v.togMacOSShowInDock.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.MacOSShowInDock, v => v.togMacOSShowInDock.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableDragDropSort, v => v.togEnableDragDropSort.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DoubleClick2Activate, v => v.togDoubleClick2Activate.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DoubleClick2Activate, v => v.togDoubleClick2Activate.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoUpdateInterval, v => v.txtautoUpdateInterval.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoUpdateInterval, v => v.txtautoUpdateInterval.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CurrentFontFamily, v => v.cmbcurrentFontFamily.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CurrentFontFamily, v => v.cmbcurrentFontFamily.Text).DisposeWith(disposables);
@@ -142,21 +143,6 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
}); });
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
case EViewAction.InitSettingFont:
await InitSettingFont();
break;
}
return await Task.FromResult(true);
}
private async Task InitSettingFont() private async Task InitSettingFont()
{ {
var lstFonts = await GetFonts(); var lstFonts = await GetFonts();
@@ -212,7 +198,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
private async void BtnBrowseCustomSystemProxyPacPath_Click(object? sender, RoutedEventArgs e) private async void BtnBrowseCustomSystemProxyPacPath_Click(object? sender, RoutedEventArgs e)
{ {
var fileName = await UI.OpenFileDialog(this, null); var fileName = await UI.OpenFileDialog(null);
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
{ {
return; return;
@@ -223,7 +209,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
private async void BtnBrowseCustomSystemProxyScriptPath_Click(object? sender, RoutedEventArgs e) private async void BtnBrowseCustomSystemProxyScriptPath_Click(object? sender, RoutedEventArgs e)
{ {
var fileName = await UI.OpenFileDialog(this, null); var fileName = await UI.OpenFileDialog(null);
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
{ {
return; return;
@@ -3,6 +3,7 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
@@ -10,32 +11,33 @@
Width="800" Width="800"
Height="450" Height="450"
x:DataType="vms:ProfilesSelectViewModel" x:DataType="vms:ProfilesSelectViewModel"
Design.DataContext="{x:Static dd:DesignData.ProfilesSelect}"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
<DockPanel Margin="8"> <DockPanel Margin="{StaticResource Margin8}">
<!-- Bottom buttons -->
<StackPanel <StackPanel
Margin="4" Margin="{StaticResource Margin4}"
HorizontalAlignment="Center" HorizontalAlignment="Center"
DockPanel.Dock="Bottom" DockPanel.Dock="Bottom"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
x:Name="btnSave" x:Name="btnSave"
Width="100" Width="100"
Click="BtnSave_Click" Content="{x:Static resx:ResUI.TbConfirm}"
Content="{x:Static resx:ResUI.TbConfirm}" /> IsDefault="True" />
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
Width="100" Width="100"
Margin="8,0" Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}" /> Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
</StackPanel> </StackPanel>
<Grid> <Grid>
<DockPanel> <DockPanel>
<!-- Top tools --> <!-- Top tools -->
<WrapPanel Margin="4" DockPanel.Dock="Top"> <WrapPanel Margin="{StaticResource Margin4}" DockPanel.Dock="Top">
<ListBox <ListBox
x:Name="lstGroup" x:Name="lstGroup"
Margin="{StaticResource MarginLr4}" Margin="{StaticResource MarginLr4}"
@@ -62,10 +64,10 @@
<TextBox <TextBox
x:Name="txtServerFilter" x:Name="txtServerFilter"
Width="200" Width="200"
Margin="8,0" Margin="{StaticResource MarginLr4}"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
Text="{Binding ServerFilter, Mode=TwoWay}" Text="{Binding ServerFilter, Mode=TwoWay}"
Watermark="{x:Static resx:ResUI.MsgServerTitle}" /> PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}" />
</WrapPanel> </WrapPanel>
<!-- Profiles grid --> <!-- Profiles grid -->
@@ -5,18 +5,11 @@ namespace v2rayN.Desktop.Views;
public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel> public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
{ {
private static Config _config;
public Task<ProfileItem?> ProfileItem => GetProfileItem();
public Task<List<ProfileItem>?> ProfileItems => GetProfileItems();
private bool _allowMultiSelect = false;
public ProfilesSelectWindow() public ProfilesSelectWindow()
{ {
InitializeComponent(); InitializeComponent();
_config = AppManager.Instance.Config; btnCancel.Click += (_, _) => Close();
btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click;
txtServerFilter.KeyDown += TxtServerFilter_KeyDown; txtServerFilter.KeyDown += TxtServerFilter_KeyDown;
lstProfiles.KeyDown += LstProfiles_KeyDown; lstProfiles.KeyDown += LstProfiles_KeyDown;
@@ -25,9 +18,6 @@ public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
lstProfiles.Sorting += LstProfiles_Sorting; lstProfiles.Sorting += LstProfiles_Sorting;
lstProfiles.DoubleTapped += LstProfiles_DoubleTapped; lstProfiles.DoubleTapped += LstProfiles_DoubleTapped;
ViewModel = new ProfilesSelectViewModel(UpdateViewHandler);
DataContext = ViewModel;
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.OneWayBind(ViewModel, vm => vm.ProfileItems, v => v.lstProfiles.ItemsSource).DisposeWith(disposables); this.OneWayBind(ViewModel, vm => vm.ProfileItems, v => v.lstProfiles.ItemsSource).DisposeWith(disposables);
@@ -35,14 +25,23 @@ public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
this.Bind(ViewModel, vm => vm.SelectedSub, v => v.lstGroup.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSub, v => v.lstGroup.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables);
});
btnCancel.Click += (s, e) => Close(false); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
this.WhenAnyValue(x => x.ViewModel.MultiSelect)
.Subscribe(AllowMultiSelect)
.DisposeWith(disposables);
ViewModel.ProfilesFocusInteraction.RegisterHandler(interaction =>
{
lstProfiles.Focus();
interaction.SetOutput(Unit.Default);
}).DisposeWith(disposables);
});
} }
public void AllowMultiSelect(bool allow) private void AllowMultiSelect(bool allow)
{ {
_allowMultiSelect = allow;
if (allow) if (allow)
{ {
lstProfiles.SelectionMode = DataGridSelectionMode.Extended; lstProfiles.SelectionMode = DataGridSelectionMode.Extended;
@@ -51,30 +50,9 @@ public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
else else
{ {
lstProfiles.SelectionMode = DataGridSelectionMode.Single; lstProfiles.SelectionMode = DataGridSelectionMode.Single;
if (lstProfiles.SelectedItems.Count > 0)
{
var first = lstProfiles.SelectedItems[0];
lstProfiles.SelectedItems.Clear();
lstProfiles.SelectedItem = first;
}
} }
} }
// Expose ConfigType filter controls to callers
public void SetConfigTypeFilter(IEnumerable<EConfigType> types, bool exclude = false)
=> ViewModel?.SetConfigTypeFilter(types, exclude);
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
}
return await Task.FromResult(true);
}
private void LstProfiles_SelectionChanged(object? sender, SelectionChangedEventArgs e) private void LstProfiles_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{ {
if (ViewModel != null) if (ViewModel != null)
@@ -124,7 +102,7 @@ public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
{ {
if (e.Key == Key.A) if (e.Key == Key.A)
{ {
if (_allowMultiSelect) if (ViewModel?.MultiSelect == true)
{ {
lstProfiles.SelectAll(); lstProfiles.SelectAll();
} }
@@ -167,22 +145,4 @@ public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
ViewModel?.RefreshServers(); ViewModel?.RefreshServers();
} }
} }
public async Task<ProfileItem?> GetProfileItem()
{
var item = await ViewModel?.GetProfileItem();
return item;
}
public async Task<List<ProfileItem>?> GetProfileItems()
{
var item = await ViewModel?.GetProfileItems();
return item;
}
private void BtnSave_Click(object sender, RoutedEventArgs e)
{
// Trigger selection finalize when Confirm is clicked
ViewModel?.SelectFinish();
}
} }
+55 -51
View File
@@ -4,6 +4,7 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
@@ -11,6 +12,7 @@
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
x:DataType="vms:ProfilesViewModel" x:DataType="vms:ProfilesViewModel"
Design.DataContext="{x:Static dd:DesignData.Profiles}"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<conv:DelayColorConverter x:Key="DelayColorConverter" /> <conv:DelayColorConverter x:Key="DelayColorConverter" />
@@ -39,60 +41,62 @@
</ListBox.ContextMenu> </ListBox.ContextMenu>
</ListBox> </ListBox>
<Button <StackPanel Orientation="Horizontal">
x:Name="btnEditSub" <Button
Margin="{StaticResource MarginLr4}" x:Name="btnEditSub"
Classes="IconButton Success" Margin="{StaticResource MarginLr4}"
ToolTip.Tip="{x:Static resx:ResUI.menuSubEdit}"> Classes="IconButton Success"
<Button.Content> ToolTip.Tip="{x:Static resx:ResUI.menuSubEdit}">
<PathIcon Data="{StaticResource SemiIconEdit}" /> <Button.Content>
</Button.Content> <PathIcon Data="{StaticResource SemiIconEdit}" />
</Button> </Button.Content>
<Button </Button>
x:Name="btnAddSub" <Button
Margin="{StaticResource MarginLr4}" x:Name="btnAddSub"
Classes="IconButton Success" Margin="{StaticResource MarginLr4}"
ToolTip.Tip="{x:Static resx:ResUI.menuSubAdd}"> Classes="IconButton Success"
<Button.Content> ToolTip.Tip="{x:Static resx:ResUI.menuSubAdd}">
<PathIcon Data="{StaticResource SemiIconPlus}" /> <Button.Content>
</Button.Content> <PathIcon Data="{StaticResource SemiIconPlus}" />
</Button> </Button.Content>
</Button>
<TextBox <TextBox
x:Name="txtServerFilter" x:Name="txtServerFilter"
Width="200" Width="200"
Margin="{StaticResource MarginLr4}" Margin="{StaticResource MarginLr4}"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
Watermark="{x:Static resx:ResUI.MsgServerTitle}" /> PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}" />
<Button <Button
x:Name="btnAutofitColumnWidth" x:Name="btnAutofitColumnWidth"
Margin="{StaticResource MarginLr4}" Margin="{StaticResource MarginLr4}"
Classes="IconButton Success" Classes="IconButton Success"
ToolTip.Tip="{x:Static resx:ResUI.menuProfileAutofitColumnWidth}"> ToolTip.Tip="{x:Static resx:ResUI.menuProfileAutofitColumnWidth}">
<Button.Content> <Button.Content>
<PathIcon Data="{StaticResource SemiIconExpand}" /> <PathIcon Data="{StaticResource SemiIconExpand}" />
</Button.Content> </Button.Content>
</Button> </Button>
<Button <Button
x:Name="btnFastRealPing" x:Name="btnFastRealPing"
Margin="{StaticResource MarginLr4}" Margin="{StaticResource MarginLr4}"
Classes="IconButton Success" Classes="IconButton Success"
ToolTip.Tip="{x:Static resx:ResUI.menuFastRealPing}"> ToolTip.Tip="{x:Static resx:ResUI.menuFastRealPing}">
<Button.Content> <Button.Content>
<PathIcon Data="{StaticResource SemiIconBolt}" /> <PathIcon Data="{StaticResource SemiIconBolt}" />
</Button.Content> </Button.Content>
</Button> </Button>
<Button <Button
x:Name="menuMixedTestServer" x:Name="menuMixedTestServer"
Margin="{StaticResource MarginLr4}" Margin="{StaticResource MarginLr4}"
Classes="IconButton Success" Classes="IconButton Success"
ToolTip.Tip="{x:Static resx:ResUI.menuMixedTestServer}"> ToolTip.Tip="{x:Static resx:ResUI.menuMixedTestServer}">
<Button.Content> <Button.Content>
<PathIcon Data="{StaticResource building_ping}" /> <PathIcon Data="{StaticResource building_ping}" />
</Button.Content> </Button.Content>
</Button> </Button>
</StackPanel>
</WrapPanel> </WrapPanel>
<DataGrid <DataGrid
+158 -191
View File
@@ -1,4 +1,6 @@
using Avalonia.VisualTree;
using DialogHostAvalonia; using DialogHostAvalonia;
using DynamicData.Binding;
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -6,20 +8,13 @@ namespace v2rayN.Desktop.Views;
public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel> public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
{ {
private static Config _config; private static Config _config;
private Window? _window;
private static readonly string _tag = "ProfilesView"; private static readonly string _tag = "ProfilesView";
public ProfilesView() public ProfilesView()
{
InitializeComponent();
}
public ProfilesView(Window window)
{ {
InitializeComponent(); InitializeComponent();
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_window = window;
menuSelectAll.Click += menuSelectAll_Click; menuSelectAll.Click += menuSelectAll_Click;
btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click;
@@ -29,16 +24,14 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
lstProfiles.DoubleTapped += LstProfiles_DoubleTapped; lstProfiles.DoubleTapped += LstProfiles_DoubleTapped;
lstProfiles.LoadingRow += LstProfiles_LoadingRow; lstProfiles.LoadingRow += LstProfiles_LoadingRow;
lstProfiles.Sorting += LstProfiles_Sorting; lstProfiles.Sorting += LstProfiles_Sorting;
//if (_config.uiItem.enableDragDropSort) if (_config.UiItem.EnableDragDropSort)
//{ {
// lstProfiles.AllowDrop = true; lstProfiles.SetValue(DragDrop.AllowDropProperty, true);
// lstProfiles.PreviewMouseLeftButtonDown += LstProfiles_PreviewMouseLeftButtonDown;
// lstProfiles.MouseMove += LstProfiles_MouseMove;
// lstProfiles.DragEnter += LstProfiles_DragEnter;
// lstProfiles.Drop += LstProfiles_Drop;
//}
ViewModel = new ProfilesViewModel(UpdateViewHandler); lstProfiles.AddHandler(PointerPressedEvent, LstProfiles_PointerPressed, RoutingStrategies.Bubble, true);
lstProfiles.AddHandler(DragDrop.DragOverEvent, LstProfiles_DragOver, RoutingStrategies.Bubble);
lstProfiles.AddHandler(DragDrop.DropEvent, LstProfiles_Drop, RoutingStrategies.Bubble);
}
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
@@ -90,17 +83,74 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.Export2InnerUriCmd, v => v.menuExport2InnerUri).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2InnerUriCmd, v => v.menuExport2InnerUri).DisposeWith(disposables);
ViewModel.ShowYesNoInteraction.RegisterHandler(async interaction =>
{
var message = interaction.Input;
var result = await UI.ShowYesNo(message);
interaction.SetOutput(result == ButtonResult.Yes);
}).DisposeWith(disposables);
ViewModel.SaveFileDialogInteraction.RegisterHandler(async interaction =>
{
var viewModel = ViewModel;
if (viewModel is null)
{
interaction.SetOutput(false);
return;
}
var profileItem = interaction.Input;
var fileName = await UI.SaveFileDialog("");
if (fileName.IsNullOrEmpty())
{
interaction.SetOutput(false);
return;
}
await viewModel.Export2ClientConfigResult(fileName, profileItem);
interaction.SetOutput(true);
}).DisposeWith(disposables);
ViewModel.SetClipboardDataInteraction.RegisterHandler(async interaction =>
{
var strData = interaction.Input;
await AvaUtils.SetClipboardData(this, strData);
interaction.SetOutput(Unit.Default);
}).DisposeWith(disposables);
ViewModel.ProfilesFocusInteraction.RegisterHandler(interaction =>
{
lstProfiles.Focus();
interaction.SetOutput(Unit.Default);
}).DisposeWith(disposables);
ViewModel.ShareServerInteraction.RegisterHandler(async interaction =>
{
var url = interaction.Input;
if (url.IsNullOrEmpty())
{
interaction.SetOutput(Unit.Default);
return;
}
await ShareServer(url);
interaction.SetOutput(Unit.Default);
}).DisposeWith(disposables);
ViewModel.DispatcherRefreshServersBizInteraction.RegisterHandler(interaction =>
{
Dispatcher.UIThread.Post(RefreshServersBiz, DispatcherPriority.Default);
interaction.SetOutput(Unit.Default);
}).DisposeWith(disposables);
ViewModel.AdjustMainLvColWidthInteraction.RegisterHandler(interaction =>
{
//AutofitColumnWidth();
interaction.SetOutput(Unit.Default);
}).DisposeWith(disposables);
AppEvents.AppExitRequested AppEvents.AppExitRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(_ => StorageUI()) .Subscribe(_ => StorageUI())
.DisposeWith(disposables); .DisposeWith(disposables);
//AppEvents.AdjustMainLvColWidthRequested
// .AsObservable()
// .ObserveOn(RxSchedulers.MainThreadScheduler)
// .Subscribe(_ => AutofitColumnWidth())
// .DisposeWith(disposables);
}); });
RestoreUI(); RestoreUI();
@@ -120,93 +170,6 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
#region Event #region Event
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.SetClipboardData:
if (obj is null)
{
return false;
}
await AvaUtils.SetClipboardData(this, (string)obj);
break;
case EViewAction.ProfilesFocus:
lstProfiles.Focus();
break;
case EViewAction.ShowYesNo:
if (await UI.ShowYesNo(_window, ResUI.RemoveServer) != ButtonResult.Yes)
{
return false;
}
break;
case EViewAction.SaveFileDialog:
if (obj is null)
{
return false;
}
var fileName = await UI.SaveFileDialog(_window, "");
if (fileName.IsNullOrEmpty())
{
return false;
}
ViewModel?.Export2ClientConfigResult(fileName, (ProfileItem)obj);
break;
case EViewAction.AddServerWindow:
if (obj is null)
{
return false;
}
return await new AddServerWindow((ProfileItem)obj).ShowDialog<bool>(_window);
case EViewAction.AddServer2Window:
if (obj is null)
{
return false;
}
return await new AddServer2Window((ProfileItem)obj).ShowDialog<bool>(_window);
case EViewAction.AddGroupServerWindow:
if (obj is null)
{
return false;
}
return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog<bool>(_window);
case EViewAction.ShareServer:
if (obj is null)
{
return false;
}
await ShareServer((string)obj);
break;
case EViewAction.SubEditWindow:
if (obj is null)
{
return false;
}
return await new SubEditWindow((SubItem)obj).ShowDialog<bool>(_window);
case EViewAction.DispatcherRefreshServersBiz:
Dispatcher.UIThread.Post(RefreshServersBiz, DispatcherPriority.Default);
break;
}
return await Task.FromResult(true);
}
public async Task ShareServer(string url) public async Task ShareServer(string url)
{ {
if (url.IsNullOrEmpty()) if (url.IsNullOrEmpty())
@@ -431,7 +394,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
} }
if (item.Name.Equals("IpInfo", StringComparison.CurrentCultureIgnoreCase)) if (item.Name.Equals("IpInfo", StringComparison.CurrentCultureIgnoreCase))
{ {
item2.IsVisible = _config.SpeedTestItem.IPAPIUrl.IsNotEmpty(); item2.IsVisible = _config.SpeedTestItem.IPAPIUrl.IsNotEmpty() && !_config.UiItem.HideColumnIpInfo;
} }
} }
} }
@@ -473,93 +436,97 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
#region Drag and Drop #region Drag and Drop
//private Point startPoint = new(); private static readonly DataFormat<object> LstProfilesRowFormat =
//private int startIndex = -1; DataFormat.CreateInProcessFormat<object>("LstProfilesRow");
//private string formatData = "ProfileItemModel";
///// <summary> private async void LstProfiles_PointerPressed(object? sender, PointerPressedEventArgs e)
///// Helper to search up the VisualTree {
///// </summary> try
///// <typeparam name="T"></typeparam> {
///// <param name="current"></param> if (e.Source is not Visual visualSource)
///// <returns></returns> {
//private static T? FindAncestor<T>(DependencyObject current) where T : DependencyObject return;
//{ }
// do
// {
// if (current is T)
// {
// return (T)current;
// }
// current = VisualTreeHelper.GetParent(current);
// }
// while (current != null);
// return null;
//}
//private void LstProfiles_PreviewMouseLeftButtonDown(object? sender, MouseButtonEventArgs e) var row = visualSource.FindAncestorOfType<DataGridRow>(true);
//{ if (row?.DataContext == null)
// // Get current mouse position {
// startPoint = e.GetPosition(null); return;
//} }
//private void LstProfiles_MouseMove(object? sender, MouseEventArgs e) if (e.GetCurrentPoint(row).Properties.IsLeftButtonPressed)
//{ {
// // Get the current mouse position var dragData = new DataTransfer();
// Point mousePos = e.GetPosition(null); var item = DataTransferItem.Create(LstProfilesRowFormat, row.DataContext);
// Vector diff = startPoint - mousePos; dragData.Add(item);
await DragDrop.DoDragDropAsync(e, dragData, DragDropEffects.Move);
}
}
catch
{
// Ignore
}
}
// if (e.LeftButton == MouseButtonState.Pressed && private void LstProfiles_DragOver(object? sender, DragEventArgs e)
// (Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance || {
// Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance)) if (!e.DataTransfer.Contains(LstProfilesRowFormat))
// { {
// // Get the dragged Item e.DragEffects = DragDropEffects.None;
// if (sender is not DataGrid listView) return; return;
// var listViewItem = FindAncestor<DataGridRow>((DependencyObject)e.OriginalSource); }
// if (listViewItem == null) return; // Abort e.DragEffects = DragDropEffects.Move;
// // Find the data behind the ListViewItem }
// ProfileItemModel item = (ProfileItemModel)listView.ItemContainerGenerator.ItemFromContainer(listViewItem);
// if (item == null) return; // Abort
// // Initialize the drag & drop operation
// startIndex = lstProfiles.SelectedIndex;
// DataObject dragData = new(formatData, item);
// DragDrop.DoDragDrop(listViewItem, dragData, DragDropEffects.Copy | DragDropEffects.Move);
// }
//}
//private void LstProfiles_DragEnter(object? sender, DragEventArgs e) private void LstProfiles_Drop(object? sender, DragEventArgs e)
//{ {
// if (!e.Data.GetDataPresent(formatData) || sender != e.Source) if (!e.DataTransfer.Contains(LstProfilesRowFormat))
// { {
// e.Effects = DragDropEffects.None; return;
// } }
//} ProfileItemModel? sourceItem = null;
foreach (var item in e.DataTransfer.Items)
{
if (!item.Formats.Contains(LstProfilesRowFormat))
{
continue;
}
if (item.TryGetRaw(LstProfilesRowFormat) is not ProfileItemModel model)
{
continue;
}
sourceItem = model;
break;
}
if (sourceItem == null)
{
return;
}
if (e.Source is not Visual visualTarget)
{
return;
}
//private void LstProfiles_Drop(object? sender, DragEventArgs e) var targetRow = visualTarget.FindAncestorOfType<DataGridRow>(true);
//{ if (targetRow is not { DataContext: ProfileItemModel targetItem })
// if (e.Data.GetDataPresent(formatData) && sender == e.Source) {
// { return;
// // Get the drop Item destination }
// if (sender is not DataGrid listView) return; if (ReferenceEquals(sourceItem, targetItem))
// var listViewItem = FindAncestor<DataGridRow>((DependencyObject)e.OriginalSource); {
// if (listViewItem == null) return;
// { }
// // Abort if (lstProfiles.ItemsSource is not IList<ProfileItemModel> items)
// e.Effects = DragDropEffects.None; {
// return; return;
// } }
// // Find the data behind the Item var oldIndex = items.IndexOf(sourceItem);
// ProfileItemModel item = (ProfileItemModel)listView.ItemContainerGenerator.ItemFromContainer(listViewItem); var newIndex = items.IndexOf(targetItem);
// if (item == null) return; if (oldIndex >= 0 && newIndex >= 0)
// // Move item into observable collection {
// // (this will be automatically reflected to lstView.ItemsSource) ViewModel?.MoveServerTo(oldIndex, targetItem);
// e.Effects = DragDropEffects.Move; }
}
// ViewModel?.MoveServerTo(startIndex, item);
// startIndex = -1;
// }
//}
#endregion Drag and Drop #endregion Drag and Drop
} }
@@ -5,8 +5,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sys="clr-namespace:System;assembly=netstandard" xmlns:sys="clr-namespace:System;assembly=netstandard"
d:DesignHeight="600"
d:DesignWidth="600"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
@@ -5,6 +5,13 @@ public partial class QrcodeView : UserControl
public QrcodeView() public QrcodeView()
{ {
InitializeComponent(); InitializeComponent();
if (Design.IsDesignMode)
{
//const string url = Global.AppName;
const string url = Global.SystemProxyExceptionsWindows;
txtContent.Text = url;
imgQrcode.Source = GetQRCode(url);
}
} }
public QrcodeView(string? url) public QrcodeView(string? url)
@@ -3,6 +3,7 @@
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: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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
@@ -10,6 +11,7 @@
Width="900" Width="900"
Height="600" Height="600"
x:DataType="vms:RoutingRuleDetailsViewModel" x:DataType="vms:RoutingRuleDetailsViewModel"
Design.DataContext="{x:Static dd:DesignData.RoutingRuleDetails}"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -92,7 +94,6 @@
<Button <Button
x:Name="btnSelectProfile" x:Name="btnSelectProfile"
Margin="0,0,8,0" Margin="0,0,8,0"
Click="BtnSelectProfile_Click"
Content="{x:Static resx:ResUI.TbSelectProfile}" /> Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock <TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"

Some files were not shown because too many files have changed in this diff Show More