mirror of
https://github.com/2dust/v2rayN.git
synced 2025-10-25 17:54:41 +00:00
Compare commits
6 commits
32334ada6b
...
8fe411744b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe411744b | ||
|
|
03d5b7a05b | ||
|
|
a652fd879b | ||
|
|
326bf334e7 | ||
|
|
21a773f400 | ||
|
|
4301415b4c |
21 changed files with 371 additions and 107 deletions
2
.github/workflows/build-linux.yml
vendored
2
.github/workflows/build-linux.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
configuration: [Release]
|
configuration: [Release]
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
|
||||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
|
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
|
||||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.6" />
|
<PackageVersion Include="Avalonia.Desktop" Version="11.3.6" />
|
||||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" />
|
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" />
|
||||||
|
|
@ -19,6 +20,7 @@
|
||||||
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
|
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
|
||||||
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
|
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
|
||||||
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.10" />
|
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.10" />
|
||||||
|
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
|
||||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.10" />
|
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.10" />
|
||||||
<PackageVersion Include="NLog" Version="6.0.4" />
|
<PackageVersion Include="NLog" Version="6.0.4" />
|
||||||
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
|
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
|
||||||
|
|
@ -27,4 +29,4 @@
|
||||||
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
|
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
|
||||||
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
|
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -87,6 +87,8 @@ public class Global
|
||||||
public const string SingboxFinalResolverTag = "final_resolver";
|
public const string SingboxFinalResolverTag = "final_resolver";
|
||||||
public const string SingboxHostsDNSTag = "hosts_dns";
|
public const string SingboxHostsDNSTag = "hosts_dns";
|
||||||
public const string SingboxFakeDNSTag = "fake_dns";
|
public const string SingboxFakeDNSTag = "fake_dns";
|
||||||
|
public const string RoutingRuleType = "Routing";
|
||||||
|
public const string DNSRuleType = "DNS";
|
||||||
|
|
||||||
public static readonly List<string> IEProxyProtocols =
|
public static readonly List<string> IEProxyProtocols =
|
||||||
[
|
[
|
||||||
|
|
@ -449,6 +451,14 @@ public class Global
|
||||||
"none"
|
"none"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static readonly Dictionary<string, string> LogLevelColors = new()
|
||||||
|
{
|
||||||
|
{ "debug", "#6C757D" },
|
||||||
|
{ "info", "#2ECC71" },
|
||||||
|
{ "warning", "#FFA500" },
|
||||||
|
{ "error", "#E74C3C" },
|
||||||
|
};
|
||||||
|
|
||||||
public static readonly List<string> InboundTags =
|
public static readonly List<string> InboundTags =
|
||||||
[
|
[
|
||||||
"socks",
|
"socks",
|
||||||
|
|
@ -471,6 +481,12 @@ public class Global
|
||||||
"tcp,udp"
|
"tcp,udp"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static readonly List<string> RuleTypes =
|
||||||
|
[
|
||||||
|
RoutingRuleType,
|
||||||
|
DNSRuleType
|
||||||
|
];
|
||||||
|
|
||||||
public static readonly List<string> destOverrideProtocols =
|
public static readonly List<string> destOverrideProtocols =
|
||||||
[
|
[
|
||||||
"http",
|
"http",
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,5 @@ public class RulesItem
|
||||||
public List<string>? Process { get; set; }
|
public List<string>? Process { get; set; }
|
||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
public string? Remarks { get; set; }
|
public string? Remarks { get; set; }
|
||||||
|
public List<string>? RuleTypes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,11 @@ public partial class CoreConfigSingboxService
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((item.RuleTypes?.Count ?? 0) > 0 && !item.RuleTypes.Contains(Global.DNSRuleType))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var rule = new Rule4Sbox();
|
var rule = new Rule4Sbox();
|
||||||
var validDomains = item.Domain.Count(it => ParseV2Domain(it, rule));
|
var validDomains = item.Domain.Count(it => ParseV2Domain(it, rule));
|
||||||
if (validDomains <= 0)
|
if (validDomains <= 0)
|
||||||
|
|
|
||||||
|
|
@ -136,13 +136,21 @@ public partial class CoreConfigSingboxService
|
||||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
|
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
|
||||||
foreach (var item1 in rules ?? [])
|
foreach (var item1 in rules ?? [])
|
||||||
{
|
{
|
||||||
if (item1.Enabled)
|
if (!item1.Enabled)
|
||||||
{
|
{
|
||||||
await GenRoutingUserRule(item1, singboxConfig);
|
continue;
|
||||||
if (item1.Ip != null && item1.Ip.Count > 0)
|
}
|
||||||
{
|
|
||||||
ipRules.Add(item1);
|
if ((item1.RuleTypes?.Count ?? 0) > 0 && !item1.RuleTypes.Contains(Global.RoutingRuleType))
|
||||||
}
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await GenRoutingUserRule(item1, singboxConfig);
|
||||||
|
|
||||||
|
if (item1.Ip?.Count > 0)
|
||||||
|
{
|
||||||
|
ipRules.Add(item1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,11 @@ public partial class CoreConfigV2rayService
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((item.RuleTypes?.Count ?? 0) > 0 && !item.RuleTypes.Contains(Global.DNSRuleType))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var domain in item.Domain)
|
foreach (var domain in item.Domain)
|
||||||
{
|
{
|
||||||
if (domain.StartsWith('#'))
|
if (domain.StartsWith('#'))
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,18 @@ public partial class CoreConfigV2rayService
|
||||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
|
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
|
||||||
foreach (var item in rules)
|
foreach (var item in rules)
|
||||||
{
|
{
|
||||||
if (item.Enabled)
|
if (!item.Enabled)
|
||||||
{
|
{
|
||||||
var item2 = JsonUtils.Deserialize<RulesItem4Ray>(JsonUtils.Serialize(item));
|
continue;
|
||||||
await GenRoutingUserRule(item2, v2rayConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((item.RuleTypes?.Count ?? 0) > 0 && !item.RuleTypes.Contains(Global.RoutingRuleType))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item2 = JsonUtils.Deserialize<RulesItem4Ray>(JsonUtils.Serialize(item));
|
||||||
|
await GenRoutingUserRule(item2, v2rayConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
|
|
@ -9,9 +10,9 @@ namespace ServiceLib.ViewModels;
|
||||||
public class MsgViewModel : MyReactiveObject
|
public class MsgViewModel : MyReactiveObject
|
||||||
{
|
{
|
||||||
private readonly ConcurrentQueue<string> _queueMsg = new();
|
private readonly ConcurrentQueue<string> _queueMsg = new();
|
||||||
private readonly int _numMaxMsg = 500;
|
private volatile bool _lastMsgFilterNotAvailable;
|
||||||
private bool _lastMsgFilterNotAvailable;
|
private int _showLock = 0; // 0 = unlocked, 1 = locked
|
||||||
private bool _blLockShow = false;
|
public int NumMaxMsg { get; } = 500;
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string MsgFilter { get; set; }
|
public string MsgFilter { get; set; }
|
||||||
|
|
@ -33,46 +34,52 @@ public class MsgViewModel : MyReactiveObject
|
||||||
this.WhenAnyValue(
|
this.WhenAnyValue(
|
||||||
x => x.AutoRefresh,
|
x => x.AutoRefresh,
|
||||||
y => y == true)
|
y => y == true)
|
||||||
.Subscribe(c => { _config.MsgUIItem.AutoRefresh = AutoRefresh; });
|
.Subscribe(c => _config.MsgUIItem.AutoRefresh = AutoRefresh);
|
||||||
|
|
||||||
AppEvents.SendMsgViewRequested
|
AppEvents.SendMsgViewRequested
|
||||||
.AsObservable()
|
.AsObservable()
|
||||||
//.ObserveOn(RxApp.MainThreadScheduler)
|
//.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
.Subscribe(async content => await AppendQueueMsg(content));
|
.Subscribe(content => _ = AppendQueueMsg(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AppendQueueMsg(string msg)
|
private async Task AppendQueueMsg(string msg)
|
||||||
{
|
{
|
||||||
//if (msg == Global.CommandClearMsg)
|
|
||||||
//{
|
|
||||||
// ClearMsg();
|
|
||||||
// return;
|
|
||||||
//}
|
|
||||||
if (AutoRefresh == false)
|
if (AutoRefresh == false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_ = EnqueueQueueMsg(msg);
|
|
||||||
|
|
||||||
if (_blLockShow)
|
EnqueueQueueMsg(msg);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!_config.UiItem.ShowInTaskbar)
|
if (!_config.UiItem.ShowInTaskbar)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_blLockShow = true;
|
if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(500);
|
try
|
||||||
var txt = string.Join("", _queueMsg.ToArray());
|
{
|
||||||
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, txt);
|
await Task.Delay(500).ConfigureAwait(false);
|
||||||
|
|
||||||
_blLockShow = false;
|
var sb = new StringBuilder();
|
||||||
|
while (_queueMsg.TryDequeue(out var line))
|
||||||
|
{
|
||||||
|
sb.Append(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _showLock, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnqueueQueueMsg(string msg)
|
private void EnqueueQueueMsg(string msg)
|
||||||
{
|
{
|
||||||
//filter msg
|
//filter msg
|
||||||
if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable)
|
if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable)
|
||||||
|
|
@ -91,26 +98,17 @@ public class MsgViewModel : MyReactiveObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Enqueue
|
|
||||||
if (_queueMsg.Count > _numMaxMsg)
|
|
||||||
{
|
|
||||||
for (int k = 0; k < _queueMsg.Count - _numMaxMsg; k++)
|
|
||||||
{
|
|
||||||
_queueMsg.TryDequeue(out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_queueMsg.Enqueue(msg);
|
_queueMsg.Enqueue(msg);
|
||||||
if (!msg.EndsWith(Environment.NewLine))
|
if (!msg.EndsWith(Environment.NewLine))
|
||||||
{
|
{
|
||||||
_queueMsg.Enqueue(Environment.NewLine);
|
_queueMsg.Enqueue(Environment.NewLine);
|
||||||
}
|
}
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearMsg()
|
//public void ClearMsg()
|
||||||
{
|
//{
|
||||||
_queueMsg.Clear();
|
// _queueMsg.Clear();
|
||||||
}
|
//}
|
||||||
|
|
||||||
private void DoMsgFilter()
|
private void DoMsgFilter()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string Process { get; set; }
|
public string Process { get; set; }
|
||||||
|
|
||||||
|
public IList<string> Types { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public bool AutoSort { get; set; }
|
public bool AutoSort { get; set; }
|
||||||
|
|
||||||
|
|
@ -51,6 +53,11 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
|
||||||
Domain = Utils.List2String(SelectedSource.Domain, true);
|
Domain = Utils.List2String(SelectedSource.Domain, true);
|
||||||
IP = Utils.List2String(SelectedSource.Ip, true);
|
IP = Utils.List2String(SelectedSource.Ip, true);
|
||||||
Process = Utils.List2String(SelectedSource.Process, true);
|
Process = Utils.List2String(SelectedSource.Process, true);
|
||||||
|
Types = SelectedSource.RuleTypes?.ToList();
|
||||||
|
if (Types == null || Types.Count == 0)
|
||||||
|
{
|
||||||
|
Types = Global.RuleTypes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveRulesAsync()
|
private async Task SaveRulesAsync()
|
||||||
|
|
@ -73,6 +80,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
|
||||||
}
|
}
|
||||||
SelectedSource.Protocol = ProtocolItems?.ToList();
|
SelectedSource.Protocol = ProtocolItems?.ToList();
|
||||||
SelectedSource.InboundTag = InboundTagItems?.ToList();
|
SelectedSource.InboundTag = InboundTagItems?.ToList();
|
||||||
|
SelectedSource.RuleTypes = Types?.ToList();
|
||||||
|
|
||||||
var hasRule = SelectedSource.Domain?.Count > 0
|
var hasRule = SelectedSource.Domain?.Count > 0
|
||||||
|| SelectedSource.Ip?.Count > 0
|
|| SelectedSource.Ip?.Count > 0
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
RequestedThemeVariant="Default">
|
RequestedThemeVariant="Default">
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<semi:SemiTheme />
|
<semi:SemiTheme />
|
||||||
|
<semi:AvaloniaEditSemiTheme />
|
||||||
<StyleInclude Source="Assets/GlobalStyles.axaml" />
|
<StyleInclude Source="Assets/GlobalStyles.axaml" />
|
||||||
<StyleInclude Source="avares://Semi.Avalonia.DataGrid/Index.axaml" />
|
<StyleInclude Source="avares://Semi.Avalonia.DataGrid/Index.axaml" />
|
||||||
<dialogHost:DialogHostStyles />
|
<dialogHost:DialogHostStyles />
|
||||||
|
|
|
||||||
129
v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs
Normal file
129
v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
using Avalonia.Media;
|
||||||
|
using AvaloniaEdit;
|
||||||
|
using AvaloniaEdit.Document;
|
||||||
|
using AvaloniaEdit.Rendering;
|
||||||
|
|
||||||
|
namespace v2rayN.Desktop.Common;
|
||||||
|
|
||||||
|
public class KeywordColorizer : DocumentColorizingTransformer
|
||||||
|
{
|
||||||
|
private readonly string[] _keywords;
|
||||||
|
private readonly Dictionary<string, IBrush> _brushMap;
|
||||||
|
|
||||||
|
public KeywordColorizer(IDictionary<string, IBrush> keywordBrushMap)
|
||||||
|
{
|
||||||
|
if (keywordBrushMap == null || keywordBrushMap.Count == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("keywordBrushMap must not be null or empty", nameof(keywordBrushMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
_brushMap = new Dictionary<string, IBrush>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var kvp in keywordBrushMap)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(kvp.Key) || kvp.Value == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_brushMap.ContainsKey(kvp.Key))
|
||||||
|
{
|
||||||
|
_brushMap[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_brushMap.Count == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("keywordBrushMap must contain at least one non-empty key with a non-null brush", nameof(keywordBrushMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
_keywords = _brushMap.Keys.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ColorizeLine(DocumentLine line)
|
||||||
|
{
|
||||||
|
var text = CurrentContext.Document.GetText(line);
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kw in _keywords)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(kw))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchStart = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var idx = text.IndexOf(kw, searchStart, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (idx < 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var kwEndIndex = idx + kw.Length;
|
||||||
|
if (IsWordCharBefore(text, idx) || IsWordCharAfter(text, kwEndIndex))
|
||||||
|
{
|
||||||
|
searchStart = idx + Math.Max(1, kw.Length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = line.Offset + idx;
|
||||||
|
var end = start + kw.Length;
|
||||||
|
|
||||||
|
if (_brushMap.TryGetValue(kw, out var brush) && brush != null)
|
||||||
|
{
|
||||||
|
ChangeLinePart(start, end, element => element.TextRunProperties.SetForegroundBrush(brush));
|
||||||
|
}
|
||||||
|
|
||||||
|
searchStart = idx + Math.Max(1, kw.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWordCharBefore(string text, int idx)
|
||||||
|
{
|
||||||
|
if (idx <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = text[idx - 1];
|
||||||
|
return char.IsLetterOrDigit(c) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWordCharAfter(string text, int idx)
|
||||||
|
{
|
||||||
|
if (idx >= text.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = text[idx];
|
||||||
|
return char.IsLetterOrDigit(c) || c == '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TextEditorKeywordHighlighter
|
||||||
|
{
|
||||||
|
public static void Attach(TextEditor editor, IDictionary<string, IBrush> keywordBrushMap)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(editor);
|
||||||
|
|
||||||
|
if (keywordBrushMap == null || keywordBrushMap.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editor.TextArea?.TextView?.LineTransformers?.OfType<KeywordColorizer>().Any() == true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorizer = new KeywordColorizer(keywordBrushMap);
|
||||||
|
editor.TextArea.TextView.LineTransformers.Add(colorizer);
|
||||||
|
editor.TextArea.TextView.InvalidateVisual();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ using Avalonia.Controls.Notifications;
|
||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
|
using AvaloniaEdit;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Semi.Avalonia;
|
using Semi.Avalonia;
|
||||||
|
|
@ -112,7 +113,8 @@ public class ThemeSettingViewModel : MyReactiveObject
|
||||||
x.OfType<ContextMenu>(),
|
x.OfType<ContextMenu>(),
|
||||||
x.OfType<DataGridRow>(),
|
x.OfType<DataGridRow>(),
|
||||||
x.OfType<ListBoxItem>(),
|
x.OfType<ListBoxItem>(),
|
||||||
x.OfType<HeaderedContentControl>()
|
x.OfType<HeaderedContentControl>(),
|
||||||
|
x.OfType<TextEditor>()
|
||||||
));
|
));
|
||||||
style.Add(new Setter()
|
style.Add(new Setter()
|
||||||
{
|
{
|
||||||
|
|
@ -153,7 +155,8 @@ public class ThemeSettingViewModel : MyReactiveObject
|
||||||
x.OfType<DataGridRow>(),
|
x.OfType<DataGridRow>(),
|
||||||
x.OfType<ListBoxItem>(),
|
x.OfType<ListBoxItem>(),
|
||||||
x.OfType<HeaderedContentControl>(),
|
x.OfType<HeaderedContentControl>(),
|
||||||
x.OfType<WindowNotificationManager>()
|
x.OfType<WindowNotificationManager>(),
|
||||||
|
x.OfType<TextEditor>()
|
||||||
));
|
));
|
||||||
style.Add(new Setter()
|
style.Add(new Setter()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
x:Class="v2rayN.Desktop.Views.MsgView"
|
x:Class="v2rayN.Desktop.Views.MsgView"
|
||||||
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: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: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"
|
||||||
|
|
@ -70,35 +71,36 @@
|
||||||
Theme="{DynamicResource SimpleToggleSwitch}" />
|
Theme="{DynamicResource SimpleToggleSwitch}" />
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
|
|
||||||
<ScrollViewer x:Name="msgScrollViewer" VerticalScrollBarVisibility="Auto">
|
<avaloniaEdit:TextEditor
|
||||||
<SelectableTextBlock
|
Name="txtMsg"
|
||||||
Name="txtMsg"
|
Margin="{StaticResource Margin8}"
|
||||||
Margin="{StaticResource Margin8}"
|
IsReadOnly="True"
|
||||||
VerticalAlignment="Stretch"
|
VerticalScrollBarVisibility="Auto"
|
||||||
Classes="TextArea"
|
WordWrap="True">
|
||||||
TextAlignment="Left"
|
<avaloniaEdit:TextEditor.Options>
|
||||||
TextWrapping="Wrap">
|
<avaloniaEdit:TextEditorOptions AllowScrollBelowDocument="False"/>
|
||||||
<SelectableTextBlock.ContextMenu>
|
</avaloniaEdit:TextEditor.Options>
|
||||||
<ContextMenu>
|
<avaloniaEdit:TextEditor.ContextFlyout>
|
||||||
<MenuItem
|
<MenuFlyout>
|
||||||
x:Name="menuMsgViewSelectAll"
|
<MenuItem
|
||||||
Click="menuMsgViewSelectAll_Click"
|
x:Name="menuMsgViewSelectAll"
|
||||||
Header="{x:Static resx:ResUI.menuMsgViewSelectAll}" />
|
Click="menuMsgViewSelectAll_Click"
|
||||||
<MenuItem
|
Header="{x:Static resx:ResUI.menuMsgViewSelectAll}" />
|
||||||
x:Name="menuMsgViewCopy"
|
<MenuItem
|
||||||
Click="menuMsgViewCopy_Click"
|
x:Name="menuMsgViewCopy"
|
||||||
Header="{x:Static resx:ResUI.menuMsgViewCopy}" />
|
Click="menuMsgViewCopy_Click"
|
||||||
<MenuItem
|
Header="{x:Static resx:ResUI.menuMsgViewCopy}" />
|
||||||
x:Name="menuMsgViewCopyAll"
|
<MenuItem
|
||||||
Click="menuMsgViewCopyAll_Click"
|
x:Name="menuMsgViewCopyAll"
|
||||||
Header="{x:Static resx:ResUI.menuMsgViewCopyAll}" />
|
Click="menuMsgViewCopyAll_Click"
|
||||||
<MenuItem
|
Header="{x:Static resx:ResUI.menuMsgViewCopyAll}" />
|
||||||
x:Name="menuMsgViewClear"
|
<MenuItem
|
||||||
Click="menuMsgViewClear_Click"
|
x:Name="menuMsgViewClear"
|
||||||
Header="{x:Static resx:ResUI.menuMsgViewClear}" />
|
Click="menuMsgViewClear_Click"
|
||||||
</ContextMenu>
|
Header="{x:Static resx:ResUI.menuMsgViewClear}" />
|
||||||
</SelectableTextBlock.ContextMenu>
|
</MenuFlyout>
|
||||||
</SelectableTextBlock>
|
</avaloniaEdit:TextEditor.ContextFlyout>
|
||||||
</ScrollViewer>
|
</avaloniaEdit:TextEditor>
|
||||||
|
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
@ -10,13 +10,11 @@ namespace v2rayN.Desktop.Views;
|
||||||
|
|
||||||
public partial class MsgView : ReactiveUserControl<MsgViewModel>
|
public partial class MsgView : ReactiveUserControl<MsgViewModel>
|
||||||
{
|
{
|
||||||
private readonly ScrollViewer _scrollViewer;
|
//private const int KeepLines = 30;
|
||||||
|
|
||||||
public MsgView()
|
public MsgView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_scrollViewer = this.FindControl<ScrollViewer>("msgScrollViewer");
|
|
||||||
|
|
||||||
ViewModel = new MsgViewModel(UpdateViewHandler);
|
ViewModel = new MsgViewModel(UpdateViewHandler);
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
|
|
@ -24,6 +22,11 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
|
||||||
|
kv => kv.Key,
|
||||||
|
kv => (IBrush)new SolidColorBrush(Color.Parse(kv.Value))
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
||||||
|
|
@ -34,9 +37,8 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
|
||||||
if (obj is null)
|
if (obj is null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() => ShowMsg(obj),
|
||||||
ShowMsg(obj),
|
DispatcherPriority.ApplicationIdle);
|
||||||
DispatcherPriority.ApplicationIdle);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return await Task.FromResult(true);
|
return await Task.FromResult(true);
|
||||||
|
|
@ -44,23 +46,37 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
|
||||||
|
|
||||||
private void ShowMsg(object msg)
|
private void ShowMsg(object msg)
|
||||||
{
|
{
|
||||||
txtMsg.Text = msg.ToString();
|
//var lineCount = txtMsg.LineCount;
|
||||||
|
//if (lineCount > ViewModel?.NumMaxMsg)
|
||||||
|
//{
|
||||||
|
// var cutLine = txtMsg.Document.GetLineByNumber(lineCount - KeepLines);
|
||||||
|
// txtMsg.Document.Remove(0, cutLine.Offset);
|
||||||
|
//}
|
||||||
|
if (txtMsg.LineCount > ViewModel?.NumMaxMsg)
|
||||||
|
{
|
||||||
|
ClearMsg();
|
||||||
|
}
|
||||||
|
|
||||||
|
txtMsg.AppendText(msg.ToString());
|
||||||
if (togScrollToEnd.IsChecked ?? true)
|
if (togScrollToEnd.IsChecked ?? true)
|
||||||
{
|
{
|
||||||
_scrollViewer?.ScrollToEnd();
|
txtMsg.ScrollToEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearMsg()
|
public void ClearMsg()
|
||||||
{
|
{
|
||||||
ViewModel?.ClearMsg();
|
txtMsg.Clear();
|
||||||
txtMsg.Text = "";
|
txtMsg.AppendText("----- Message cleared -----\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void menuMsgViewSelectAll_Click(object? sender, RoutedEventArgs e)
|
private void menuMsgViewSelectAll_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
txtMsg.Focus();
|
Dispatcher.UIThread.Post(() =>
|
||||||
txtMsg.SelectAll();
|
{
|
||||||
|
txtMsg.TextArea.Focus();
|
||||||
|
txtMsg.SelectAll();
|
||||||
|
}, DispatcherPriority.Render);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void menuMsgViewCopy_Click(object? sender, RoutedEventArgs e)
|
private async void menuMsgViewCopy_Click(object? sender, RoutedEventArgs e)
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,25 @@
|
||||||
Width="300"
|
Width="300"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left" />
|
HorizontalAlignment="Left" />
|
||||||
<ToggleSwitch
|
<StackPanel
|
||||||
x:Name="togEnabled"
|
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<ToggleSwitch
|
||||||
|
x:Name="togEnabled"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<ListBox
|
||||||
|
x:Name="clbRuleTypes"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
SelectionMode="Multiple,Toggle"
|
||||||
|
Theme="{DynamicResource CardCheckGroupListBox}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
|
|
@ -58,17 +70,15 @@
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
Orientation="Horizontal"
|
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center"
|
||||||
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSelectProfile"
|
x:Name="btnSelectProfile"
|
||||||
Margin="0,0,8,0"
|
Margin="0,0,8,0"
|
||||||
Content="{x:Static resx:ResUI.TbSelectProfile}"
|
Click="BtnSelectProfile_Click"
|
||||||
Click="BtnSelectProfile_Click" />
|
Content="{x:Static resx:ResUI.TbSelectProfile}" />
|
||||||
<TextBlock
|
<TextBlock VerticalAlignment="Center" Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
|
||||||
VerticalAlignment="Center"
|
|
||||||
Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,16 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
|
||||||
clbInboundTag.ItemsSource = Global.InboundTags;
|
clbInboundTag.ItemsSource = Global.InboundTags;
|
||||||
cmbNetwork.ItemsSource = Global.RuleNetworks;
|
cmbNetwork.ItemsSource = Global.RuleNetworks;
|
||||||
|
|
||||||
|
clbRuleTypes.SelectionChanged += ClbRuleTypes_SelectionChanged;
|
||||||
|
clbRuleTypes.ItemsSource = Global.RuleTypes;
|
||||||
|
if (ViewModel.Types != null)
|
||||||
|
{
|
||||||
|
foreach (var it in ViewModel.Types)
|
||||||
|
{
|
||||||
|
clbRuleTypes.SelectedItems.Add(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!rulesItem.Id.IsNullOrEmpty())
|
if (!rulesItem.Id.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
rulesItem.Protocol?.ForEach(it =>
|
rulesItem.Protocol?.ForEach(it =>
|
||||||
|
|
@ -108,4 +118,12 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ClbRuleTypes_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel != null)
|
||||||
|
{
|
||||||
|
ViewModel.Types = clbRuleTypes.SelectedItems.Cast<string>().ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia.AvaloniaEdit" />
|
||||||
<PackageReference Include="Avalonia.Controls.DataGrid">
|
<PackageReference Include="Avalonia.Controls.DataGrid">
|
||||||
<TreatAsUsed>true</TreatAsUsed>
|
<TreatAsUsed>true</TreatAsUsed>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" />
|
<PackageReference Include="Avalonia.ReactiveUI" />
|
||||||
<PackageReference Include="MessageBox.Avalonia" />
|
<PackageReference Include="MessageBox.Avalonia" />
|
||||||
<PackageReference Include="Semi.Avalonia" />
|
<PackageReference Include="Semi.Avalonia" />
|
||||||
|
<PackageReference Include="Semi.Avalonia.AvaloniaEdit" />
|
||||||
<PackageReference Include="Semi.Avalonia.DataGrid">
|
<PackageReference Include="Semi.Avalonia.DataGrid">
|
||||||
<TreatAsUsed>true</TreatAsUsed>
|
<TreatAsUsed>true</TreatAsUsed>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
|
||||||
|
|
@ -47,19 +47,22 @@ public partial class MsgView
|
||||||
|
|
||||||
private void ShowMsg(object msg)
|
private void ShowMsg(object msg)
|
||||||
{
|
{
|
||||||
txtMsg.BeginChange();
|
if (txtMsg.LineCount > ViewModel?.NumMaxMsg)
|
||||||
txtMsg.Text = msg.ToString();
|
{
|
||||||
|
ClearMsg();
|
||||||
|
}
|
||||||
|
|
||||||
|
txtMsg.AppendText(msg.ToString());
|
||||||
if (togScrollToEnd.IsChecked ?? true)
|
if (togScrollToEnd.IsChecked ?? true)
|
||||||
{
|
{
|
||||||
txtMsg.ScrollToEnd();
|
txtMsg.ScrollToEnd();
|
||||||
}
|
}
|
||||||
txtMsg.EndChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearMsg()
|
public void ClearMsg()
|
||||||
{
|
{
|
||||||
ViewModel?.ClearMsg();
|
|
||||||
txtMsg.Clear();
|
txtMsg.Clear();
|
||||||
|
txtMsg.AppendText("----- Message cleared -----\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void menuMsgViewSelectAll_Click(object sender, System.Windows.RoutedEventArgs e)
|
private void menuMsgViewSelectAll_Click(object sender, System.Windows.RoutedEventArgs e)
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,25 @@
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Style="{StaticResource DefTextBox}" />
|
Style="{StaticResource DefTextBox}" />
|
||||||
<ToggleButton
|
<StackPanel
|
||||||
x:Name="togEnabled"
|
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<ToggleButton
|
||||||
|
x:Name="togEnabled"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<ListBox
|
||||||
|
x:Name="clbRuleTypes"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
FontSize="{DynamicResource StdFontSize}"
|
||||||
|
Style="{StaticResource MaterialDesignFilterChipPrimaryListBox}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,16 @@ public partial class RoutingRuleDetailsWindow
|
||||||
clbInboundTag.ItemsSource = Global.InboundTags;
|
clbInboundTag.ItemsSource = Global.InboundTags;
|
||||||
cmbNetwork.ItemsSource = Global.RuleNetworks;
|
cmbNetwork.ItemsSource = Global.RuleNetworks;
|
||||||
|
|
||||||
|
clbRuleTypes.SelectionChanged += ClbRuleTypes_SelectionChanged;
|
||||||
|
clbRuleTypes.ItemsSource = Global.RuleTypes;
|
||||||
|
if (ViewModel.Types != null)
|
||||||
|
{
|
||||||
|
foreach (var it in ViewModel.Types)
|
||||||
|
{
|
||||||
|
clbRuleTypes.SelectedItems.Add(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!rulesItem.Id.IsNullOrEmpty())
|
if (!rulesItem.Id.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
rulesItem.Protocol?.ForEach(it =>
|
rulesItem.Protocol?.ForEach(it =>
|
||||||
|
|
@ -101,4 +111,12 @@ public partial class RoutingRuleDetailsWindow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ClbRuleTypes_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel != null)
|
||||||
|
{
|
||||||
|
ViewModel.Types = clbRuleTypes.SelectedItems.Cast<string>().ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue