~ruther/NosTale-PacketLogger

c6ddfeca9a2c5bb6dc8dc8048a80182226efa649 — Rutherther 2 years ago 86b762f
feat: allow editing only no profile in profile choose
M src/PacketLogger/Models/Filters/FilterProfiles.cs => src/PacketLogger/Models/Filters/FilterProfiles.cs +6 -1
@@ 29,6 29,11 @@ public class FilterProfiles
        AllProfiles = new ObservableCollection<FilterProfile>();
        SelectableProfiles = new ObservableCollection<FilterProfile>();

        SelectableProfiles.Add(new FilterProfile(false)
        {
            Name = "No profile"
        });

        AllProfiles.Add(DefaultProfile);
    }



@@ 74,7 79,7 @@ public class FilterProfiles
    /// <param name="profile">The profile to add.</param>
    public void AddProfile(FilterProfile profile)
    {
        SelectableProfiles.Add(profile);
        SelectableProfiles.Insert(SelectableProfiles.Count - 1, profile);
        AllProfiles.Add(profile);
    }


M src/PacketLogger/ViewModels/Filters/FilterChooseViewModel.cs => src/PacketLogger/ViewModels/Filters/FilterChooseViewModel.cs +72 -2
@@ 7,6 7,7 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using DynamicData;
using DynamicData.Binding;
using PacketLogger.Models.Filters;
using ReactiveUI;


@@ 20,6 21,7 @@ public class FilterChooseViewModel : ViewModelBase, IDisposable
{
    private FilterProfile _currentProfile = null!;
    private IDisposable? _cleanUp;
    private FilterProfile _noProfile;

    /// <summary>
    /// Initializes a new instance of the <see cref="FilterChooseViewModel"/> class.


@@ 30,6 32,10 @@ public class FilterChooseViewModel : ViewModelBase, IDisposable
        RecvFilterSelected = true;
        CurrentProfile = currentProfile;
        CurrentFilter = CreateSendRecvFilter();
        _noProfile = new FilterProfile(false)
        {
            Name = "No profile"
        };
    }

    /// <summary>


@@ 40,9 46,59 @@ public class FilterChooseViewModel : ViewModelBase, IDisposable
        get => _currentProfile;
        set
        {
            if (value is null)
            {
                return;
            }

            if (value.Name == "No profile" && value != _noProfile)
            {
                CurrentProfile = _noProfile;
                return;
            }

            var lastProfile = value;
            if (value != _noProfile)
            {
                RecvEntryViewModel = new FilterEntryViewModel
                (
                    value.RecvFilterEntry,
                    (data) =>
                    {
                        CopyCurrentToNoProfile(lastProfile);
                        _noProfile.RecvFilterEntry.Filters.Add(data);
                        CurrentProfile = _noProfile;
                    },
                    (data) =>
                    {
                        CopyCurrentToNoProfile(lastProfile);
                        _noProfile.RecvFilterEntry.Filters.Remove(data);
                        CurrentProfile = _noProfile;
                    }
                );
                SendEntryViewModel = new FilterEntryViewModel
                (
                    value.SendFilterEntry,
                    (data) =>
                    {
                        CopyCurrentToNoProfile(lastProfile);
                        _noProfile.SendFilterEntry.Filters.Add(data);
                        CurrentProfile = _noProfile;
                    },
                    (data) =>
                    {
                        CopyCurrentToNoProfile(lastProfile);
                        _noProfile.SendFilterEntry.Filters.Remove(data);
                        CurrentProfile = _noProfile;
                    }
                );
            }
            else
            {
                RecvEntryViewModel = new FilterEntryViewModel(value.RecvFilterEntry, null, null);
                SendEntryViewModel = new FilterEntryViewModel(value.SendFilterEntry, null, null);
            }
            _currentProfile = value;
            RecvEntryViewModel = new FilterEntryViewModel(_currentProfile.RecvFilterEntry);
            SendEntryViewModel = new FilterEntryViewModel(_currentProfile.SendFilterEntry);

            var recvWhenAny = _currentProfile.RecvFilterEntry.WhenAnyPropertyChanged("Active", "Whitelist")
                .Subscribe((e) => OnChange());


@@ 62,6 118,20 @@ public class FilterChooseViewModel : ViewModelBase, IDisposable
        }
    }

    private void CopyCurrentToNoProfile(FilterProfile lastProfile)
    {
        _noProfile.RecvFilterEntry.Filters.Clear();
        _noProfile.RecvFilterEntry.Filters.AddRange(lastProfile.RecvFilterEntry.Filters);

        _noProfile.SendFilterEntry.Filters.Clear();
        _noProfile.SendFilterEntry.Filters.AddRange(lastProfile.SendFilterEntry.Filters);

        _noProfile.RecvFilterEntry.Active = lastProfile.RecvFilterEntry.Active;
        _noProfile.RecvFilterEntry.Whitelist = lastProfile.RecvFilterEntry.Whitelist;
        _noProfile.SendFilterEntry.Active = lastProfile.SendFilterEntry.Active;
        _noProfile.SendFilterEntry.Whitelist = lastProfile.SendFilterEntry.Whitelist;
    }

    /// <summary>
    /// Gets the current recv entry view model.
    /// </summary>

M src/PacketLogger/ViewModels/Filters/FilterConfigViewModel.cs => src/PacketLogger/ViewModels/Filters/FilterConfigViewModel.cs +2 -2
@@ 22,8 22,8 @@ public class FilterConfigViewModel : ViewModelBase
    public FilterConfigViewModel(FilterProfile filterProfile)
    {
        _filterProfile = filterProfile;
        RecvEntryViewModel = new FilterEntryViewModel(filterProfile.RecvFilterEntry);
        SendEntryViewModel = new FilterEntryViewModel(filterProfile.SendFilterEntry);
        RecvEntryViewModel = new FilterEntryViewModel(filterProfile.RecvFilterEntry, null, null);
        SendEntryViewModel = new FilterEntryViewModel(filterProfile.SendFilterEntry, null, null);
    }

    /// <summary>

M src/PacketLogger/ViewModels/Filters/FilterEntryViewModel.cs => src/PacketLogger/ViewModels/Filters/FilterEntryViewModel.cs +29 -7
@@ 4,6 4,7 @@
//  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.Reactive;
using PacketLogger.Models.Filters;
using ReactiveUI;


@@ 19,7 20,10 @@ public class FilterEntryViewModel : ViewModelBase
    /// Initializes a new instance of the <see cref="FilterEntryViewModel"/> class.
    /// </summary>
    /// <param name="entry">The profile entry.</param>
    public FilterEntryViewModel(FilterProfileEntry entry)
    /// <param name="addNew">The action called upon adding new.</param>
    /// <param name="remove">The action called upon removing the given filter data.</param>
    public FilterEntryViewModel
        (FilterProfileEntry entry, Action<FilterCreator.FilterData>? addNew, Action<FilterCreator.FilterData>? remove)
    {
        NewFilterType = FilterCreator.FilterType.PacketHeader;
        Entry = entry;


@@ 30,14 34,24 @@ public class FilterEntryViewModel : ViewModelBase
                var selected = SelectedFilter;
                if (selected is not null)
                {
                    var selectedIndex = Entry.Filters.IndexOf(selected);
                    SelectedFilter = Entry.Filters.Count > selectedIndex + 1 ? Entry.Filters[selectedIndex + 1] : null;
                    if (SelectedFilter is null && selectedIndex > 0)
                    if (remove is not null)
                    {
                        SelectedFilter = Entry.Filters[selectedIndex - 1];
                        SelectedFilter = null;
                        remove(selected);
                    }
                    else
                    {
                        var selectedIndex = Entry.Filters.IndexOf(selected);
                        SelectedFilter = Entry.Filters.Count > selectedIndex + 1
                            ? Entry.Filters[selectedIndex + 1]
                            : null;
                        if (SelectedFilter is null && selectedIndex > 0)
                        {
                            SelectedFilter = Entry.Filters[selectedIndex - 1];
                        }

                    Entry.Filters.Remove(selected);
                        Entry.Filters.Remove(selected);
                    }
                }
            }
        );


@@ 49,7 63,15 @@ public class FilterEntryViewModel : ViewModelBase
                if (!string.IsNullOrEmpty(NewFilter))
                {
                    var newFilter = new FilterCreator.FilterData(NewFilterType, NewFilter);
                    Entry.Filters.Add(newFilter);
                    if (addNew is not null)
                    {
                        addNew(newFilter);
                    }
                    else
                    {
                        Entry.Filters.Add(newFilter);
                    }

                    NewFilter = string.Empty;
                    SelectedFilter = newFilter;
                }

A src/PacketLogger/ViewModels/Log/PacketLogViewModel.cs => src/PacketLogger/ViewModels/Log/PacketLogViewModel.cs +212 -0
@@ 0,0 1,212 @@
//
//  PacketLogViewModel.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.Concurrency;
using System.Reactive.Linq;
using Avalonia;
using DynamicData;
using DynamicData.Binding;
using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;
using PacketLogger.ViewModels.Filters;
using ReactiveUI;

namespace PacketLogger.ViewModels.Log;

/// <inheritdoc />
public class PacketLogViewModel : ViewModelBase, IDisposable
{
    private readonly FilterProfiles _filterProfiles;
    private readonly ReadOnlyObservableCollection<PacketInfo> _packets;
    private readonly IDisposable _cleanUp;
    private bool _logReceived = true;
    private bool _logSent = true;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketLogViewModel"/> class.
    /// </summary>
    /// <param name="packetProvider">The packet provider.</param>
    /// <param name="filterProfiles">The filter profiles.</param>
    public PacketLogViewModel(IPacketProvider packetProvider, FilterProfiles filterProfiles)
    {
        _filterProfiles = filterProfiles;
        FilterChoose = new FilterChooseViewModel(new FilterProfile(false));
        FilterChoose.CurrentProfile = filterProfiles.DefaultFilterEnabled
            ? filterProfiles.DefaultProfile
            : new FilterProfile(false)
            {
                Name = "No profile"
            };
        Provider = packetProvider;

        var dynamicFilter = FilterChoose.WhenValueChanged(x => x.CurrentFilter)
            .Select
            (
                filter =>
                {
                    return (Func<PacketInfo, bool>)((pi) =>
                    {
                        if (filter is null)
                        {
                            return true;
                        }

                        return filter.Match(pi);
                    });
                }
            );

        var packetsSubscription = Provider.Packets.Connect()
            .Filter(dynamicFilter)
            .Sort(new PacketComparer())
            .Bind(out _packets)
            .ObserveOn(RxApp.MainThreadScheduler)
            .DisposeMany()
            .Subscribe
            (
                _ =>
                {
                    if (Scroll)
                    {
                        RxApp.MainThreadScheduler.Schedule
                        (
                            DateTimeOffset.Now.AddMilliseconds(100),
                            () =>
                            {
                                if (FilteredPackets.Count > 0)
                                {
                                    SelectedPacket = FilteredPackets[^1];
                                }
                            }
                        );
                    }
                }
            );

        _cleanUp = packetsSubscription;
        CopyPackets = ReactiveCommand.CreateFromObservable<IList, Unit>
        (
            list => Observable.StartAsync
            (
                async () =>
                {
                    var clipboardString = string.Join
                        ('\n', list.OfType<PacketInfo>().Select(x => x.PacketString));
                    await Application.Current!.Clipboard!.SetTextAsync(clipboardString);
                }
            )
        );

        TogglePane = ReactiveCommand.Create<Unit, bool>
        (
            _ => PaneOpen = !PaneOpen
        );

        Clear = ReactiveCommand.Create
        (
            () => Provider.Clear()
        );
    }

    /// <summary>
    /// Gets filter profiles.
    /// </summary>
    public FilterProfiles Profiles => _filterProfiles;

    /// <summary>
    /// Gets the filtered packets.
    /// </summary>
    public ReadOnlyObservableCollection<PacketInfo> FilteredPackets => _packets;

    /// <summary>
    /// Gets packet provider.
    /// </summary>
    public IPacketProvider Provider { get; }

    /// <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 => _logReceived;
        set
        {
            Provider.LogReceived = value;
            _logReceived = value;
        }
    }

    /// <summary>
    /// Gets or sets whether to log sent packets.
    /// </summary>
    public bool LogSent
    {
        get => _logSent;
        set
        {
            Provider.LogSent = value;
            _logSent = value;
        }
    }

    /// <summary>
    /// Gets or sets whether to scroll to teh bottom of the grid.
    /// </summary>
    public bool Scroll { get; set; } = true;

    /// <summary>
    /// Gets or sets the currently selected packet.
    /// </summary>
    public object? SelectedPacket { get; set; }

    /// <summary>
    /// Gets empty string.
    /// </summary>
    public string Empty { get; } = string.Empty;

    /// <summary>
    /// Gets the filter choose view model.
    /// </summary>
    public FilterChooseViewModel FilterChoose { get; }

    /// <inheritdoc />
    public void Dispose()
    {
        TogglePane.Dispose();
        CopyPackets.Dispose();
        Clear.Dispose();
        Provider.Dispose();
        (Provider as CommsPacketProvider)?.CustomDispose();
        _cleanUp.Dispose();
    }
}
\ No newline at end of file

A src/PacketLogger/Views/Log/PacketLogView.axaml => src/PacketLogger/Views/Log/PacketLogView.axaml +100 -0
@@ 0,0 1,100 @@
<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.Log.PacketLogView"
             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"
             xmlns:log="clr-namespace:PacketLogger.ViewModels.Log"
             x:Name="UserControl">
    <UserControl.Resources>
        <converters:PacketSourceConverter x:Key="packetSourceConverter" />
    </UserControl.Resources>
    <Design.DataContext>
        <log:PacketLogViewModel />
    </Design.DataContext>
    <SplitView OpenPaneLength="300" IsPaneOpen="{Binding PaneOpen, Mode = TwoWay}" DisplayMode="CompactInline"
               PanePlacement="Right">
        <SplitView.Pane>
            <Grid Width="280" HorizontalAlignment="Left"
                  ColumnDefinitions="*" RowDefinitions="Auto,*,Auto" Margin="10">
                <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Stretch">
                    <Button Grid.Row="0" 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" FontSize="30" Text="Filter"
                               Margin="5,0,0,0" />

                    <ComboBox Width="160" Margin="10,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Stretch"
                              Items="{Binding Profiles.SelectableProfiles}" SelectedItem="{Binding FilterChoose.CurrentProfile, Mode = TwoWay}">
                        <ComboBox.DataTemplates>
                            <DataTemplate>
                                <TextBlock Text="{Binding Name}" />
                            </DataTemplate>
                        </ComboBox.DataTemplates>
                    </ComboBox>
                </StackPanel>

                <ContentControl Grid.Row="1" Content="{Binding FilterChoose}" />

                <Grid Grid.Row="2" 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>

        <ListBox Items="{Binding FilteredPackets}"
                 x:Name="PacketsLog"
                 SelectionMode="Multiple"
                 SelectedItem="{Binding SelectedPacket, Mode=TwoWay}"
                 VerticalAlignment="Stretch"
                 SelectionChanged="PacketsLog_OnSelectionChanged">
            <ListBox.Styles>
                <Style Selector="ListBoxItem">
                    <Setter Property="Padding" Value="1" />
                </Style>
                <Style Selector="TextBlock">
                    <Setter Property="FontSize" Value="12" />
                </Style>
            </ListBox.Styles>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Height="16" Margin="0" Orientation="Horizontal">
                        <TextBlock Width="60" Text="{Binding Date, StringFormat = {}{0:HH:mm:ss}}" />
                        <TextBlock Width="40"
                                   Text="{Binding Source, Converter = {StaticResource packetSourceConverter}}">
                        </TextBlock>
                        <Border ToolTip.Tip="{Binding PacketString}">
                            <TextBlock VerticalAlignment="Center" Text="{Binding PacketString}"
                                       TextTrimming="CharacterEllipsis">
                            </TextBlock>
                        </Border>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ContextMenu>
                <ContextMenu Name="PacketMenu">
                    <MenuItem Header="Copy packets" Command="{Binding CopyPackets}"
                              CommandParameter="{Binding ElementName=PacketsLog, Path=SelectedItems}" IsEnabled="True">
                    </MenuItem>
                </ContextMenu>
            </ListBox.ContextMenu>
        </ListBox>
    </SplitView>
</UserControl>
\ No newline at end of file

A src/PacketLogger/Views/Log/PacketLogView.axaml.cs => src/PacketLogger/Views/Log/PacketLogView.axaml.cs +37 -0
@@ 0,0 1,37 @@
//
//  PacketLogView.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.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PropertyChanged;

namespace PacketLogger.Views.Log;

[DoNotNotify]
public partial class PacketLogView : UserControl
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketLogView"/> class.
    /// </summary>
    public PacketLogView()
    {
        InitializeComponent();
    }

    private void InitializeComponent()
    {
        AvaloniaXamlLoader.Load(this);
    }

    private void PacketsLog_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
    {
        if (sender is DataGrid dataGrid && dataGrid.SelectedItem is not null)
        {
            dataGrid.ScrollIntoView(dataGrid.SelectedItem, dataGrid.Columns.First());
        }
    }
}
\ No newline at end of file

Do not follow this link