~ruther/NosTale-PacketLogger

af5d083f0d3372331734fa8648e2e6883a430a1c — František Boháček 2 years ago 18df69c
feat: add log tab mvvm
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

Do not follow this link