A src/PacketLogger/Converters/PacketSourceConverter.cs => src/PacketLogger/Converters/PacketSourceConverter.cs +61 -0
@@ 0,0 1,61 @@
+//
+// PacketSourceConverter.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using NosSmooth.PacketSerializer.Abstractions.Attributes;
+
+namespace PacketLogger.Converters;
+
+/// <summary>
+/// Converts <see cref="PacketSource"/>.
+/// </summary>
+public class PacketSourceConverter : IValueConverter
+{
+ /// <inheritdoc />
+ public object? Convert
+ (
+ object? value,
+ Type targetType,
+ object? parameter,
+ CultureInfo culture
+ )
+ {
+ if (value is not PacketSource source)
+ {
+ throw new ArgumentException("Must be PacketSource.", nameof(value));
+ }
+
+ return source == PacketSource.Client ? "Send" : "Recv";
+ }
+
+ /// <inheritdoc />
+ public object? ConvertBack
+ (
+ object? value,
+ Type targetType,
+ object? parameter,
+ CultureInfo culture
+ )
+ {
+ if (value is not string stringVal)
+ {
+ throw new ArgumentException("Must be string.", nameof(value));
+ }
+
+ if (stringVal == "Send")
+ {
+ return PacketSource.Client;
+ }
+ if (stringVal == "Recv")
+ {
+ return PacketSource.Server;
+ }
+
+ throw new ArgumentException("Must be \"Recv\" or \"Send\".");
+ }
+}<
\ No newline at end of file
A src/PacketLogger/ViewModels/LogFilterTabViewModel.cs => src/PacketLogger/ViewModels/LogFilterTabViewModel.cs +96 -0
@@ 0,0 1,96 @@
+//
+// LogFilterTabViewModel.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.ObjectModel;
+using System.Reactive;
+using Avalonia;
+using Avalonia.Input.Platform;
+using DynamicData;
+using Microsoft.VisualBasic;
+using PacketLogger.Models.Filters;
+using ReactiveUI;
+
+namespace PacketLogger.ViewModels;
+
+/// <inheritdoc />
+public class LogFilterTabViewModel : ViewModelBase, IDisposable
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LogFilterTabViewModel"/> class.
+ /// </summary>
+ public LogFilterTabViewModel()
+ {
+ Filters = new ObservableCollection<FilterCreator.FilterData>();
+ RemoveCurrent = ReactiveCommand.Create
+ (
+ () =>
+ {
+ var selected = SelectedFilter;
+ if (selected is not null)
+ {
+ SelectedFilter = null;
+ Filters.Remove(selected);
+ }
+ }
+ );
+
+ AddNew = ReactiveCommand.Create
+ (
+ () =>
+ {
+ if (!string.IsNullOrEmpty(NewFilter))
+ {
+ Filters.Add(new FilterCreator.FilterData(NewFilterType, NewFilter));
+ NewFilter = string.Empty;
+ }
+ }
+ );
+
+ }
+
+ /// <summary>
+ /// Gets or sets whether the filters should whitelist or blacklist.
+ /// </summary>
+ public bool Whitelist { get; set; }
+
+ /// <summary>
+ /// Gets or sets the currently selected filter.
+ /// </summary>
+ public FilterCreator.FilterData? SelectedFilter { get; set; }
+
+ /// <summary>
+ /// Gets or sets the filters list.
+ /// </summary>
+ public ObservableCollection<FilterCreator.FilterData> Filters { get; }
+
+ /// <summary>
+ /// Gets or sets the type of the new filter to add.
+ /// </summary>
+ public FilterCreator.FilterType NewFilterType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the string of the new filter to add.
+ /// </summary>
+ public string NewFilter { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets the command to remove the currently selected filter.
+ /// </summary>
+ public ReactiveCommand<Unit, Unit> RemoveCurrent { get; }
+
+ /// <summary>
+ /// Gets the command to add new filter.
+ /// </summary>
+ public ReactiveCommand<Unit, Unit> AddNew { get; }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ RemoveCurrent.Dispose();
+ AddNew.Dispose();
+ }
+}<
\ No newline at end of file
A src/PacketLogger/ViewModels/LogTabViewModel.cs => src/PacketLogger/ViewModels/LogTabViewModel.cs +194 -0
@@ 0,0 1,194 @@
+//
+// LogTabViewModel.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using Avalonia;
+using DynamicData;
+using DynamicData.Binding;
+using PacketLogger.Models.Filters;
+using PacketLogger.Models.Packets;
+using ReactiveUI;
+
+namespace PacketLogger.ViewModels;
+
+/// <inheritdoc />
+public class LogTabViewModel : ViewModelBase, IDisposable
+{
+ private readonly ReadOnlyObservableCollection<PacketInfo> _packets;
+ private IDisposable _packetsSubscription;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LogTabViewModel"/> class.
+ /// </summary>
+ public LogTabViewModel()
+ {
+ var dynamicFilter = this.WhenValueChanged(@this => @this.CurrentFilter)
+ .Select
+ (
+ filter =>
+ {
+ return (Func<PacketInfo, bool>)((pi) =>
+ {
+ if (filter is null)
+ {
+ return true;
+ }
+
+ return filter.Match(pi);
+ });
+ }
+ );
+
+ _packetsSubscription = Provider.Packets.Connect()
+ .Filter(dynamicFilter)
+ .Sort(SortExpressionComparer<PacketInfo>.Ascending(x => x.PacketIndex))
+ .Bind(out _packets)
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .DisposeMany()
+ .Subscribe();
+
+ CopyPackets = ReactiveCommand.CreateFromObservable<IList, Unit>
+ (
+ (l) => Observable.StartAsync
+ (
+ async () =>
+ {
+ var clipboardString = string.Join
+ ('\n', l.OfType<PacketInfo>().Select(x => x.PacketString));
+ await Application.Current!.Clipboard!.SetTextAsync(clipboardString);
+ }
+ )
+ );
+
+ TogglePane = ReactiveCommand.Create<Unit, bool>
+ (
+ _ => PaneOpen = !PaneOpen
+ );
+
+ Clear = ReactiveCommand.Create
+ (
+ () => Provider.Clear()
+ );
+
+ SendFilter = new();
+ RecvFilter = new();
+
+ SendFilter.PropertyChanged += (s, e) =>
+ {
+ if (e.PropertyName == "Whitelist")
+ {
+ CreateSendRecv();
+ }
+ };
+ RecvFilter.PropertyChanged += (s, e) =>
+ {
+ if (e.PropertyName == "Whitelist")
+ {
+ CreateSendRecv();
+ }
+ };
+ SendFilter.Filters.CollectionChanged += (s, e) => { CreateSendRecv(); };
+ RecvFilter.Filters.CollectionChanged += (s, e) => { CreateSendRecv(); };
+ }
+
+ /// <summary>
+ /// Gets the send filter model.
+ /// </summary>
+ public LogFilterTabViewModel SendFilter { get; }
+
+ /// <summary>
+ /// Gets the receive filter model.
+ /// </summary>
+ public LogFilterTabViewModel RecvFilter { get; }
+
+ /// <summary>
+ /// Gets the currently applied filter.
+ /// </summary>
+ public IFilter? CurrentFilter { get; private set; }
+
+ /// <summary>
+ /// Gets the filtered packets.
+ /// </summary>
+ public ReadOnlyObservableCollection<PacketInfo> FilteredPackets => _packets;
+
+ /// <summary>
+ /// Gets packet provider.
+ /// </summary>
+ public IPacketProvider Provider { get; } = new DummyPacketProvider();
+
+ /// <summary>
+ /// Gets whether the pane is open.
+ /// </summary>
+ public bool PaneOpen { get; private set; } = true;
+
+ /// <summary>
+ /// Gets the toggle pane command.
+ /// </summary>
+ public ReactiveCommand<Unit, bool> TogglePane { get; }
+
+ /// <summary>
+ /// Gets command to copy packets.
+ /// </summary>
+ public ReactiveCommand<IList, Unit> CopyPackets { get; }
+
+ /// <summary>
+ /// Gets the command for clearing.
+ /// </summary>
+ public ReactiveCommand<Unit, Unit> Clear { get; }
+
+ /// <summary>
+ /// Gets or sets whether to log received packets.
+ /// </summary>
+ public bool LogReceived { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets whether to log sent packets.
+ /// </summary>
+ public bool LogSent { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets whether to scroll to teh bottom of the grid.
+ /// </summary>
+ public bool Scroll { get; set; } = true;
+
+ /// <summary>
+ /// Gets empty string.
+ /// </summary>
+ public string Empty { get; } = string.Empty;
+
+ private void CreateSendRecv()
+ {
+ IFilter recvFilter = CreateCompound(RecvFilter);
+ IFilter sendFilter = CreateCompound(SendFilter);
+
+ CurrentFilter = new SendRecvFilter(sendFilter, recvFilter);
+ }
+
+ private IFilter CreateCompound(LogFilterTabViewModel logFilterTab)
+ {
+ List<IFilter> filters = new List<IFilter>();
+
+ foreach (var filter in logFilterTab.Filters)
+ {
+ filters.Add(FilterCreator.BuildFilter(filter.Type, filter.Value));
+ }
+
+ return new CompoundFilter(!logFilterTab.Whitelist, filters.ToArray());
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ TogglePane.Dispose();
+ _packetsSubscription.Dispose();
+ }
+}<
\ No newline at end of file
A src/PacketLogger/Views/LogFilterTab.axaml => src/PacketLogger/Views/LogFilterTab.axaml +37 -0
@@ 0,0 1,37 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="clr-namespace:PacketLogger.ViewModels"
+ mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="450"
+ x:Class="PacketLogger.Views.LogFilterTab">
+ <Design.DataContext>
+ <vm:LogFilterTabViewModel />
+ </Design.DataContext>
+
+ <Grid ColumnDefinitions="*,*" RowDefinitions="*, 40, 40, 40">
+ <DataGrid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Items="{Binding Filters}" IsReadOnly="True"
+ SelectedItem="{Binding SelectedFilter}"
+ CanUserReorderColumns="True" CanUserSortColumns="True" CanUserResizeColumns="True">
+ <DataGrid.Columns>
+ <DataGridTextColumn Header="Type" Binding="{Binding Type}" MinWidth="80" />
+ <DataGridTextColumn Header="Value" Binding="{Binding Value}" Width="*" MinWidth="100" />
+ </DataGrid.Columns>
+ </DataGrid>
+
+ <RadioButton Grid.Row="1" Grid.Column="0" Content="Whitelist" GroupName="WlBl"></RadioButton>
+ <RadioButton Grid.Row="1" Grid.Column="1" Content="Blacklist" GroupName="WlBl" IsChecked="{Binding !Whitelist}"></RadioButton>
+
+ <TextBox VerticalAlignment="Center" Margin="0, 0, 5, 0" Height="30" Grid.Row="2" Grid.Column="0"
+ Text="{Binding NewFilter}">
+ </TextBox>
+ <ComboBox x:Name="FilterType" VerticalAlignment="Center" HorizontalAlignment="Stretch" Margin="5, 0, 0, 0"
+ Height="32" Grid.Row="2"
+ Grid.Column="1" SelectedItem="{Binding NewFilterType}" />
+
+ <Button HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0, 0, 5, 0" Grid.Row="3"
+ Grid.Column="0" Content="Remove" Command="{Binding RemoveCurrent}" />
+ <Button HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="5, 0, 0,0" Grid.Row="3"
+ Grid.Column="1" Content="Add" Command="{Binding AddNew}" IsDefault="True" />
+ </Grid>
+</UserControl><
\ No newline at end of file
A src/PacketLogger/Views/LogFilterTab.axaml.cs => src/PacketLogger/Views/LogFilterTab.axaml.cs +32 -0
@@ 0,0 1,32 @@
+//
+// LogFilterTab.axaml.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using PacketLogger.Models.Filters;
+using PropertyChanged;
+
+namespace PacketLogger.Views;
+
+[DoNotNotify]
+public partial class LogFilterTab : UserControl
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LogFilterTab"/> class.
+ /// </summary>
+ public LogFilterTab()
+ {
+ InitializeComponent();
+ this.FindControl<ComboBox>("FilterType").Items = Enum.GetValues<FilterCreator.FilterType>();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}<
\ No newline at end of file
A src/PacketLogger/Views/LogTab.axaml => src/PacketLogger/Views/LogTab.axaml +99 -0
@@ 0,0 1,99 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="clr-namespace:PacketLogger.ViewModels"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="PacketLogger.Views.LogTab"
+ xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
+ xmlns:converters="clr-namespace:PacketLogger.Converters"
+ xmlns:views="clr-namespace:PacketLogger.Views"
+ xmlns:packets="clr-namespace:PacketLogger.Models.Packets"
+ xmlns:packetLogger="clr-namespace:PacketLogger"
+ x:Name="UserControl">
+ <UserControl.Resources>
+ <converters:PacketSourceConverter x:Key="packetSourceConverter" />
+ </UserControl.Resources>
+ <Design.DataContext>
+ <vm:LogTabViewModel />
+ </Design.DataContext>
+ <SplitView IsPaneOpen="{Binding PaneOpen, Mode = TwoWay}" DisplayMode="CompactInline" PanePlacement="Right">
+ <SplitView.Pane>
+ <Grid ColumnDefinitions="*" RowDefinitions="0.8*,100" Margin="10">
+ <Grid Grid.Row="0" RowDefinitions="30,*">
+ <Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="24, *">
+ <Button Margin="0,1,0,0" VerticalContentAlignment="Stretch"
+ HorizontalContentAlignment="Stretch" Width="22" Height="22"
+ Command="{Binding TogglePane}">
+ <Grid>
+ <i:Icon Value="mdi-menu-left" Height="22" Width="22" Margin="0,0,2,0"
+ IsVisible="{Binding !PaneOpen}" />
+ <i:Icon Value="mdi-menu-right" Height="22" Width="22" IsVisible="{Binding PaneOpen}" />
+ </Grid>
+ </Button>
+ <TextBlock VerticalAlignment="Center" Grid.Column="1" FontSize="30" Text="Filter"
+ Margin="5,0,0,0" />
+ </Grid>
+
+ <TabControl Grid.Row="1" Margin="0" Padding="0">
+ <TabItem>
+ <TabItem.Header>
+ Recv
+ </TabItem.Header>
+
+ <views:LogFilterTab DataContext="{Binding RecvFilter}" />
+ </TabItem>
+ <TabItem>
+ <TabItem.Header>
+ Send
+ </TabItem.Header>
+
+ <views:LogFilterTab DataContext="{Binding SendFilter}" />
+ </TabItem>
+ </TabControl>
+ </Grid>
+
+ <Grid Grid.Row="1" RowDefinitions="40,40" ColumnDefinitions="140,140">
+ <CheckBox Grid.Row="0" Grid.Column="0" Content="Log received" IsChecked="{Binding LogReceived}" />
+ <CheckBox Grid.Row="0" Grid.Column="1" Content="Log sent" IsChecked="{Binding LogSent}" />
+ <CheckBox Grid.Row="1" Grid.Column="0" Content="Scroll" IsChecked="{Binding Scroll}" />
+ <Button Grid.Row="1" Grid.Column="1" Content="Clear" HorizontalAlignment="Stretch"
+ HorizontalContentAlignment="Center" Command="{Binding Clear}" />
+ </Grid>
+ </Grid>
+ </SplitView.Pane>
+
+ <DataGrid Items="{Binding FilteredPackets}" IsReadOnly="True" CanUserSortColumns="False"
+ x:Name="PacketsLog"
+ CanUserReorderColumns="False">
+ <DataGrid.Columns>
+ <DataGridTextColumn Header="Time" Binding="{Binding Date, StringFormat = {}{0:HH:mm:ss}}"
+ ClipboardContentBinding="{Binding ElementName=UserControl, Path= DataContext.Empty}"
+ Width="90" />
+ <DataGridTextColumn Header="Source"
+ Binding="{Binding Source, Converter = {StaticResource packetSourceConverter}}"
+ ClipboardContentBinding="{Binding ElementName=UserControl, Path= DataContext.Empty}"
+ Width="85" />
+ <DataGridTemplateColumn Header="Packet"
+ ClipboardContentBinding="{Binding PacketString}" Width="*">
+ <DataGridTemplateColumn.CellTemplate>
+ <DataTemplate>
+ <Border Margin="5,0,0,0" ToolTip.Tip="{Binding PacketString}">
+ <TextBlock VerticalAlignment="Center" Text="{Binding PacketString}"
+ TextTrimming="CharacterEllipsis">
+ </TextBlock>
+ </Border>
+ </DataTemplate>
+ </DataGridTemplateColumn.CellTemplate>
+ </DataGridTemplateColumn>
+ </DataGrid.Columns>
+ <DataGrid.ContextMenu>
+ <ContextMenu Name="PacketMenu">
+ <MenuItem Header="Copy packets" Command="{Binding CopyPackets}"
+ CommandParameter="{Binding ElementName=PacketsLog, Path=SelectedItems}" IsEnabled="True">
+ </MenuItem>
+ </ContextMenu>
+ </DataGrid.ContextMenu>
+ </DataGrid>
+ </SplitView>
+</UserControl><
\ No newline at end of file
A src/PacketLogger/Views/LogTab.axaml.cs => src/PacketLogger/Views/LogTab.axaml.cs +29 -0
@@ 0,0 1,29 @@
+//
+// LogTab.axaml.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using PropertyChanged;
+
+namespace PacketLogger.Views;
+
+[DoNotNotify]
+public partial class LogTab : UserControl
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LogTab"/> class.
+ /// </summary>
+ public LogTab()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}<
\ No newline at end of file