mirror of
				https://github.com/2dust/v2rayN.git
				synced 2025-10-26 10:14:42 +00:00 
			
		
		
		
	Compare commits
	
		
			5 commits
		
	
	
		
			32334ada6b
			...
			8fe411744b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 8fe411744b | ||
|   | 03d5b7a05b | ||
|   | a652fd879b | ||
|   | 326bf334e7 | ||
|   | 21a773f400 | 
					 11 changed files with 249 additions and 85 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" /> | ||||||
|  |  | ||||||
|  | @ -451,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", | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue