~ruther/NosTale-PacketLogger

86b762f16a33f04280b48e0c61c4585ce1e4b3ec — František Boháček 2 years ago d5d462c
feat: add settings and filter profiles
32 files changed, 807 insertions(+), 447 deletions(-)

A src/PacketLogger/Models/Filters/FilterProfile.cs
A src/PacketLogger/Models/Filters/FilterProfileEntry.cs
A src/PacketLogger/Models/Filters/FilterProfiles.cs
M src/PacketLogger/PacketLogger.csproj
M src/PacketLogger/ViewModels/DockFactory.cs
M src/PacketLogger/ViewModels/DocumentViewModel.cs
A src/PacketLogger/ViewModels/Filters/FilterChooseViewModel.cs
A src/PacketLogger/ViewModels/Filters/FilterConfigViewModel.cs
R src/PacketLogger/ViewModels/{PacketLogFilterViewModel.cs => Filters/FilterEntryViewModel.cs}
M src/PacketLogger/ViewModels/MainWindowViewModel.cs
D src/PacketLogger/ViewModels/PacketLogViewModel.cs
R src/PacketLogger/ViewModels/{PacketSendSubViewModel.cs => Sender/PacketSendSubViewModel.cs}
R src/PacketLogger/ViewModels/{PacketSenderViewModel.cs => Sender/PacketSenderViewModel.cs}
A src/PacketLogger/ViewModels/Settings/FilterSettingsViewModel.cs
A src/PacketLogger/ViewModels/Settings/SettingViewModelBase.cs
A src/PacketLogger/ViewModels/Settings/SettingsViewModel.cs
A src/PacketLogger/Views/Filters/FilterChooseView.axaml
A src/PacketLogger/Views/Filters/FilterChooseView.axaml.cs
A src/PacketLogger/Views/Filters/FilterConfigView.axaml
A src/PacketLogger/Views/Filters/FilterConfigView.axaml.cs
R src/PacketLogger/Views/{PacketLogFilterView.axaml => Filters/FilterEntryView.axaml}
R src/PacketLogger/Views/{PacketLogFilterView.axaml.cs => Filters/FilterEntryView.axaml.cs}
M src/PacketLogger/Views/MainWindow.axaml
D src/PacketLogger/Views/PacketLogView.axaml
R src/PacketLogger/Views/{PacketSendSubView.axaml => Sender/PacketSendSubView.axaml}
R src/PacketLogger/Views/{PacketSendSubView.axaml.cs => Sender/PacketSendSubView.axaml.cs}
R src/PacketLogger/Views/{PacketSenderView.axaml => Sender/PacketSenderView.axaml}
R src/PacketLogger/Views/{PacketSenderView.axaml.cs => Sender/PacketSenderView.axaml.cs}
A src/PacketLogger/Views/Settings/FilterSettingsView.axaml
R src/PacketLogger/Views/{PacketLogView.axaml.cs => Settings/FilterSettingsView.axaml.cs}
A src/PacketLogger/Views/Settings/SettingsView.axaml
A src/PacketLogger/Views/Settings/SettingsView.axaml.cs
A src/PacketLogger/Models/Filters/FilterProfile.cs => src/PacketLogger/Models/Filters/FilterProfile.cs +50 -0
@@ 0,0 1,50 @@
//
//  FilterProfile.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.Collections.ObjectModel;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using ReactiveUI;

namespace PacketLogger.Models.Filters;

/// <summary>
/// A filter profile.
/// </summary>
public class FilterProfile : ObservableObject
{
    /// <summary>
    /// Initializes a new instance of the <see cref="FilterProfile"/> class.
    /// </summary>
    /// <param name="isDefault">Whether this profile is a default profile.</param>
    public FilterProfile(bool isDefault)
    {
        IsDefault = isDefault;
        Name = "New filter";
        RecvFilterEntry = new FilterProfileEntry();
        SendFilterEntry = new FilterProfileEntry();
    }

    /// <summary>
    /// Gets whether this profile is a default profile.
    /// </summary>
    public bool IsDefault { get; }

    /// <summary>
    /// Gets or sets the name of the filter profile.
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Gets or sets the receive filter entry.
    /// </summary>
    public FilterProfileEntry RecvFilterEntry { get; }

    /// <summary>
    /// Gets or sets the send filter entry.
    /// </summary>
    public FilterProfileEntry SendFilterEntry { get; }
}
\ No newline at end of file

A src/PacketLogger/Models/Filters/FilterProfileEntry.cs => src/PacketLogger/Models/Filters/FilterProfileEntry.cs +40 -0
@@ 0,0 1,40 @@
//
//  FilterProfileEntry.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.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;

namespace PacketLogger.Models.Filters;

/// <summary>
/// Receive of send entry of a <see cref="FilterProfile"/>.
/// </summary>
public class FilterProfileEntry : ObservableObject
{
    /// <summary>
    /// Initializes a new instance of the <see cref="FilterProfileEntry"/> class.
    /// </summary>
    public FilterProfileEntry()
    {
        Active = true;
        Filters = new ObservableCollection<FilterCreator.FilterData>();
    }

    /// <summary>
    /// Gets or sets whether the filter is active.
    /// </summary>
    public bool Active { get; set; }

    /// <summary>
    /// Gets or sets whether the filters should whitelist or blacklist.
    /// </summary>
    public bool Whitelist { get; set; }

    /// <summary>
    /// Gets or sets the filters list.
    /// </summary>
    public ObservableCollection<FilterCreator.FilterData> Filters { get; }
}
\ No newline at end of file

A src/PacketLogger/Models/Filters/FilterProfiles.cs => src/PacketLogger/Models/Filters/FilterProfiles.cs +90 -0
@@ 0,0 1,90 @@
//
//  FilterProfiles.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.Collections.ObjectModel;
using System.Reactive.Linq;

namespace PacketLogger.Models.Filters;

/// <summary>
/// A collection of <see cref="FilterProfile"/>s.
/// </summary>
public class FilterProfiles
{
    private bool _defaultFilterEnabled;

    /// <summary>
    /// Initializes a new instance of the <see cref="FilterProfiles"/> class.
    /// </summary>
    public FilterProfiles()
    {
        DefaultProfile = new FilterProfile(true)
        {
            Name = "Default"
        };

        AllProfiles = new ObservableCollection<FilterProfile>();
        SelectableProfiles = new ObservableCollection<FilterProfile>();

        AllProfiles.Add(DefaultProfile);
    }

    /// <summary>
    /// Gets or sets whether the default filter is enabled.
    /// </summary>
    public bool DefaultFilterEnabled
    {
        get => _defaultFilterEnabled;
        set
        {
            if (!_defaultFilterEnabled && value)
            {
                SelectableProfiles.Insert(0, DefaultProfile);
            }
            else if (_defaultFilterEnabled && !value)
            {
                SelectableProfiles.Remove(DefaultProfile);
            }

            _defaultFilterEnabled = value;
        }
    }

    /// <summary>
    /// Gets or sets the default filter.
    /// </summary>
    public FilterProfile DefaultProfile { get; }

    /// <summary>
    /// Gets or sets the collection of profiles.
    /// </summary>
    public ObservableCollection<FilterProfile> SelectableProfiles { get; }

    /// <summary>
    /// Gets or sets the collection of profiles.
    /// </summary>
    public ObservableCollection<FilterProfile> AllProfiles { get; }

    /// <summary>
    /// Add the given profile.
    /// </summary>
    /// <param name="profile">The profile to add.</param>
    public void AddProfile(FilterProfile profile)
    {
        SelectableProfiles.Add(profile);
        AllProfiles.Add(profile);
    }

    /// <summary>
    /// Remove the given filter.
    /// </summary>
    /// <param name="profile">The profile to remove.</param>
    public void RemoveProfile(FilterProfile profile)
    {
        SelectableProfiles.Remove(profile);
        AllProfiles.Remove(profile);
    }
}
\ No newline at end of file

M src/PacketLogger/PacketLogger.csproj => src/PacketLogger/PacketLogger.csproj +27 -0
@@ 42,4 42,31 @@
    <PackageReference Include="Remora.Results" Version="7.2.3" />
    <PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
  </ItemGroup>

  <ItemGroup>
    <Compile Update="Views\Sender\PacketSendSubView.axaml.cs">
      <DependentUpon>PacketSendSubView.axaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
    <Compile Update="Views\Sender\PacketSenderView.axaml.cs">
      <DependentUpon>PacketSenderView.axaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
    <Compile Update="Views\Log\PacketLogView.axaml.cs">
      <DependentUpon>PacketLogView.axaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
    <Compile Update="Views\Settings\SettingsView.axaml.cs">
      <DependentUpon>SettingsView.axaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
    <Compile Update="Views\Filters\FilterChooseView.axaml.cs">
      <DependentUpon>FilterChooseView.axaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
    <Compile Update="Views\Filters\FilterConfigView.axaml.cs">
      <DependentUpon>FilterConfigView.axaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
  </ItemGroup>
</Project>

M src/PacketLogger/ViewModels/DockFactory.cs => src/PacketLogger/ViewModels/DockFactory.cs +8 -0
@@ 16,6 16,7 @@ using Dock.Model.Mvvm.Controls;
using NosSmooth.Comms.Local;
using NosSmooth.Core.Stateful;
using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;
using PacketLogger.Views;
using ReactiveUI;


@@ 29,6 30,7 @@ namespace PacketLogger.ViewModels;
public class DockFactory : Factory, IDisposable
{
    private readonly StatefulRepository _repository;
    private readonly FilterProfiles _filterProfiles;
    private readonly ObservableCollection<IPacketProvider> _providers;
    private readonly NostaleProcesses _processes;
    private readonly CommsInjector _injector;


@@ 39,18 41,21 @@ public class DockFactory : Factory, IDisposable
    /// <summary>
    /// Initializes a new instance of the <see cref="DockFactory"/> class.
    /// </summary>
    /// <param name="filterProfiles">The filter profiles.</param>
    /// <param name="providers">The providers.</param>
    /// <param name="processes">The nostale processes.</param>
    /// <param name="injector">The communications injector.</param>
    /// <param name="repository">The repository.</param>
    public DockFactory
    (
        FilterProfiles filterProfiles,
        ObservableCollection<IPacketProvider> providers,
        NostaleProcesses processes,
        CommsInjector injector,
        StatefulRepository repository
    )
    {
        _filterProfiles = filterProfiles;
        _providers = providers;
        _processes = processes;
        _repository = repository;


@@ 96,6 101,7 @@ public class DockFactory : Factory, IDisposable

        var document = new DocumentViewModel
            (
                _filterProfiles,
                _injector,
                _repository,
                _providers,


@@ 138,6 144,7 @@ public class DockFactory : Factory, IDisposable
                var index = documentDock.VisibleDockables?.Count + 1;
                var document = new DocumentViewModel
                    (
                        _filterProfiles,
                        _injector,
                        _repository,
                        _providers,


@@ 160,6 167,7 @@ public class DockFactory : Factory, IDisposable
    {
        var initialTab = new DocumentViewModel
            (
                _filterProfiles,
                _injector,
                _repository,
                _providers,

M src/PacketLogger/ViewModels/DocumentViewModel.cs => src/PacketLogger/ViewModels/DocumentViewModel.cs +28 -5
@@ 25,7 25,11 @@ using NosSmooth.Core.Contracts;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Stateful;
using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;
using PacketLogger.ViewModels.Log;
using PacketLogger.ViewModels.Sender;
using PacketLogger.ViewModels.Settings;
using ReactiveUI;
using Remora.Results;



@@ 34,7 38,6 @@ namespace PacketLogger.ViewModels;
/// <inheritdoc />
public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
{
    private readonly CommsInjector _injector;
    private readonly ObservableCollection<IPacketProvider> _providers;
    private readonly NostaleProcesses _processes;
    private readonly Action<DocumentViewModel> _onDocumentUnloaded;


@@ 45,6 48,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
    /// <summary>
    /// Initializes a new instance of the <see cref="DocumentViewModel"/> class.
    /// </summary>
    /// <param name="filterProfiles">The filter profiles.</param>
    /// <param name="injector">The injector.</param>
    /// <param name="repository">The repository.</param>
    /// <param name="providers">The providers.</param>


@@ 53,6 57,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
    /// <param name="onDocumentUnloaded">The action to call on document unloaded/closed.</param>
    public DocumentViewModel
    (
        FilterProfiles filterProfiles,
        CommsInjector injector,
        StatefulRepository repository,
        ObservableCollection<IPacketProvider> providers,


@@ 61,8 66,8 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
        Action<DocumentViewModel> onDocumentUnloaded
    )
    {
        FilterProfiles = filterProfiles;
        _ctSource = new CancellationTokenSource();
        _injector = injector;
        _providers = providers;
        _processes = processes;
        _onDocumentUnloaded = onDocumentUnloaded;


@@ 74,7 79,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
                {
                    Loading = true;
                    _packetProvider = new DummyPacketProvider(Title);
                    NestedViewModel = new PacketLogViewModel(_packetProvider);
                    NestedViewModel = new PacketLogViewModel(_packetProvider, filterProfiles);
                    Loaded = true;
                    onDocumentLoaded(this);
                }


@@ 112,7 117,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
                }

                Title = Path.GetFileName(path);
                NestedViewModel = new PacketLogViewModel(provider);
                NestedViewModel = new PacketLogViewModel(provider, filterProfiles);
                Loaded = true;
                Loading = false;
                onDocumentLoaded(this);


@@ 171,7 176,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
                    )
                    .Subscribe();

                NestedViewModel = new PacketLogViewModel(provider);
                NestedViewModel = new PacketLogViewModel(provider, filterProfiles);
                Title = handshakeResponse.CharacterName ?? $"Not in game ({process.Process.Id})";
                Loading = false;
                Loaded = true;


@@ 192,9 197,22 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
        );

        ClearError = ReactiveCommand.Create(() => Error = null);

        OpenSettings = ReactiveCommand.Create(
            () =>
            {
                Title = "Settings";
                NestedViewModel = new SettingsViewModel(filterProfiles);
                Loaded = true;
            });
    }

    /// <summary>
    /// Gets the filter profiles.
    /// </summary>
    public FilterProfiles FilterProfiles { get; }

    /// <summary>
    /// Gets the processes observable.
    /// </summary>
    public ObservableCollection<NostaleProcess> Processes => _processes.Processes;


@@ 259,6 277,11 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
    /// </summary>
    public ReactiveCommand<NostaleProcess, Unit> OpenProcess { get; }

    /// <summary>
    /// Get open settings command.
    /// </summary>
    public ReactiveCommand<Unit, Unit> OpenSettings { get; }

    /// <inheritdoc />
    public override bool OnClose()
    {

A src/PacketLogger/ViewModels/Filters/FilterChooseViewModel.cs => src/PacketLogger/ViewModels/Filters/FilterChooseViewModel.cs +129 -0
@@ 0,0 1,129 @@
//
//  FilterChooseViewModel.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.Generic;
using System.Reactive.Disposables;
using DynamicData.Binding;
using PacketLogger.Models.Filters;
using ReactiveUI;

namespace PacketLogger.ViewModels.Filters;

/// <summary>
/// A view model for filter choose view.
/// </summary>
public class FilterChooseViewModel : ViewModelBase, IDisposable
{
    private FilterProfile _currentProfile = null!;
    private IDisposable? _cleanUp;

    /// <summary>
    /// Initializes a new instance of the <see cref="FilterChooseViewModel"/> class.
    /// </summary>
    /// <param name="currentProfile">The current filter profile.</param>
    public FilterChooseViewModel(FilterProfile currentProfile)
    {
        RecvFilterSelected = true;
        CurrentProfile = currentProfile;
        CurrentFilter = CreateSendRecvFilter();
    }

    /// <summary>
    /// Gets the current profile.
    /// </summary>
    public FilterProfile CurrentProfile
    {
        get => _currentProfile;
        set
        {
            _currentProfile = value;
            RecvEntryViewModel = new FilterEntryViewModel(_currentProfile.RecvFilterEntry);
            SendEntryViewModel = new FilterEntryViewModel(_currentProfile.SendFilterEntry);

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

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

            var recvFilters = _currentProfile.RecvFilterEntry.Filters.ObserveCollectionChanges()
                .Subscribe((e) => OnChange());

            var sendFilters = _currentProfile.SendFilterEntry.Filters.ObserveCollectionChanges()
                .Subscribe((e) => OnChange());

            _cleanUp?.Dispose();
            _cleanUp = new CompositeDisposable(recvWhenAny, sendWhenAny, recvFilters, sendFilters);
            OnChange();
        }
    }

    /// <summary>
    /// Gets the current recv entry view model.
    /// </summary>
    public FilterEntryViewModel RecvEntryViewModel { get; private set; } = null!;

    /// <summary>
    /// Gets the current send entry view model.
    /// </summary>
    public FilterEntryViewModel SendEntryViewModel { get; private set; } = null!;

    /// <summary>
    /// Gets whether the send filter is currently selected.
    /// </summary>
    public bool SendFilterSelected { get; set; }

    /// <summary>
    /// Gets whether the recv filter is currently selected.
    /// </summary>
    public bool RecvFilterSelected { get; set; }

    /// <summary>
    /// Gets the currently selected filter.
    /// </summary>
    public IFilter CurrentFilter { get; private set; }

    private void OnChange()
    {
        CurrentFilter = CreateSendRecvFilter();
    }

    /// <summary>
    /// Create a filter out of the chosen filters.
    /// </summary>
    /// <returns>The created filter.</returns>
    public IFilter CreateSendRecvFilter()
    {
        IFilter recvFilter = CreateCompound(RecvEntryViewModel.Entry);
        IFilter sendFilter = CreateCompound(SendEntryViewModel.Entry);

        return new SendRecvFilter(sendFilter, recvFilter);
    }

    private IFilter CreateCompound(FilterProfileEntry filterEntry)
    {
        if (!filterEntry.Active)
        {
            return new CompoundFilter(true);
        }

        List<IFilter> filters = new List<IFilter>();

        foreach (var filter in filterEntry.Filters)
        {
            filters.Add(FilterCreator.BuildFilter(filter.Type, filter.Value));
        }

        return new CompoundFilter(!filterEntry.Whitelist, filters.ToArray());
    }

    /// <inheritdoc />
    public void Dispose()
    {
        _cleanUp?.Dispose();
    }
}
\ No newline at end of file

A src/PacketLogger/ViewModels/Filters/FilterConfigViewModel.cs => src/PacketLogger/ViewModels/Filters/FilterConfigViewModel.cs +43 -0
@@ 0,0 1,43 @@
//
//  FilterConfigViewModel.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 PacketLogger.Models.Filters;

namespace PacketLogger.ViewModels.Filters;

/// <summary>
/// A view model for FilterConfigView.
/// </summary>
public class FilterConfigViewModel : ViewModelBase
{
    private readonly FilterProfile _filterProfile;

    /// <summary>
    /// Initializes a new instance of the <see cref="FilterConfigViewModel"/> class.
    /// </summary>
    /// <param name="filterProfile">The filter profile to show and configure.</param>
    public FilterConfigViewModel(FilterProfile filterProfile)
    {
        _filterProfile = filterProfile;
        RecvEntryViewModel = new FilterEntryViewModel(filterProfile.RecvFilterEntry);
        SendEntryViewModel = new FilterEntryViewModel(filterProfile.SendFilterEntry);
    }

    /// <summary>
    /// Gets the filter profile.
    /// </summary>
    public FilterProfile Profile => _filterProfile;

    /// <summary>
    /// Gets the recv entry view model.
    /// </summary>
    public FilterEntryViewModel RecvEntryViewModel { get; }

    /// <summary>
    /// Gets the send entry view model.
    /// </summary>
    public FilterEntryViewModel SendEntryViewModel { get; }
}
\ No newline at end of file

R src/PacketLogger/ViewModels/PacketLogFilterViewModel.cs => src/PacketLogger/ViewModels/Filters/FilterEntryViewModel.cs +18 -37
@@ 1,30 1,28 @@
//
//  PacketLogFilterViewModel.cs
//  FilterEntryViewModel.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;
namespace PacketLogger.ViewModels.Filters;

/// <inheritdoc />
public class PacketLogFilterViewModel : ViewModelBase, IDisposable
/// <summary>
/// A view model for FilterEntryView.
/// </summary>
public class FilterEntryViewModel : ViewModelBase
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketLogFilterViewModel"/> class.
    /// Initializes a new instance of the <see cref="FilterEntryViewModel"/> class.
    /// </summary>
    public PacketLogFilterViewModel()
    /// <param name="entry">The profile entry.</param>
    public FilterEntryViewModel(FilterProfileEntry entry)
    {
        Filters = new ObservableCollection<FilterCreator.FilterData>();
        NewFilterType = FilterCreator.FilterType.PacketHeader;
        Entry = entry;
        RemoveCurrent = ReactiveCommand.Create
        (
            () =>


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

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


@@ 51,7 49,7 @@ public class PacketLogFilterViewModel : ViewModelBase, IDisposable
                if (!string.IsNullOrEmpty(NewFilter))
                {
                    var newFilter = new FilterCreator.FilterData(NewFilterType, NewFilter);
                    Filters.Add(newFilter);
                    Entry.Filters.Add(newFilter);
                    NewFilter = string.Empty;
                    SelectedFilter = newFilter;
                }


@@ 60,9 58,9 @@ public class PacketLogFilterViewModel : ViewModelBase, IDisposable
    }

    /// <summary>
    /// Gets or sets whether the filters should whitelist or blacklist.
    /// Gets the filter profile entry associated with this view model.
    /// </summary>
    public bool Whitelist { get; set; }
    public FilterProfileEntry Entry { get; }

    /// <summary>
    /// Gets or sets the currently selected filter.


@@ 70,11 68,6 @@ public class PacketLogFilterViewModel : ViewModelBase, IDisposable
    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; }


@@ 93,16 86,4 @@ public class PacketLogFilterViewModel : ViewModelBase, IDisposable
    /// Gets the command to add new filter.
    /// </summary>
    public ReactiveCommand<Unit, Unit> AddNew { get; }

    /// <summary>
    /// Gets or sets whether the filter is active.
    /// </summary>
    public bool Active { get; set; } = true;

    /// <inheritdoc />
    public void Dispose()
    {
        RemoveCurrent.Dispose();
        AddNew.Dispose();
    }
}
\ No newline at end of file

M src/PacketLogger/ViewModels/MainWindowViewModel.cs => src/PacketLogger/ViewModels/MainWindowViewModel.cs +12 -0
@@ 12,6 12,7 @@ using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reflection;
using Avalonia;
using Avalonia.Controls;


@@ 25,7 26,9 @@ using NosSmooth.PacketSerializer.Abstractions.Attributes;
using NosSmooth.PacketSerializer.Extensions;
using NosSmooth.PacketSerializer.Packets;
using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;
using PacketLogger.ViewModels.Log;
using ReactiveUI;

namespace PacketLogger.ViewModels;


@@ 43,6 46,7 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
    {
        var services = new ServiceCollection()
            .AddLogging(b => b.ClearProviders().AddConsole())
            .AddSingleton<FilterProfiles>()
            .AddSingleton<DockFactory>()
            .AddSingleton<NostaleProcesses>()
            .AddSingleton<ObservableCollection<IPacketProvider>>(_ => Providers)


@@ 164,6 168,9 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
        OpenEmpty = ReactiveCommand.Create
            (() => _factory.CreateLoadedDocument(doc => doc.OpenDummy.Execute(Unit.Default)));

        OpenSettings = ReactiveCommand.Create
            (() => _factory.CreateLoadedDocument(doc => doc.OpenSettings.Execute(Unit.Default)));

        Connect = ReactiveCommand.Create<IList>
            (process => _factory.CreateLoadedDocument(doc => doc.OpenProcess.Execute((NostaleProcess)process[0]!)));



@@ 231,4 238,9 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
    /// Gets the command that opens a new tab.
    /// </summary>
    public ReactiveCommand<Unit, Unit> NewTab { get; }

    /// <summary>
    /// Gets the command used for opening settings.
    /// </summary>
    public ReactiveCommand<Unit, Unit> OpenSettings { get; }
}
\ No newline at end of file

D src/PacketLogger/ViewModels/PacketLogViewModel.cs => src/PacketLogger/ViewModels/PacketLogViewModel.cs +0 -267
@@ 1,267 0,0 @@
//
//  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.Disposables;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Collections;
using DynamicData;
using DynamicData.Binding;
using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;
using ReactiveUI;
using Reloaded.Memory.Kernel32;

namespace PacketLogger.ViewModels;

/// <inheritdoc />
public class PacketLogViewModel : ViewModelBase, IDisposable
{
    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>
    public PacketLogViewModel(IPacketProvider packetProvider)
    {
        Provider = packetProvider;

        var dynamicFilter = this.WhenValueChanged(@this => @this.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()
        );

        SendFilter = new();
        RecvFilter = new();

        SendFilter.PropertyChanged += (s, e) =>
        {
            if (e.PropertyName is "Whitelist" or "Active")
            {
                CreateSendRecv();
            }
        };
        RecvFilter.PropertyChanged += (s, e) =>
        {
            if (e.PropertyName is "Whitelist" or "Active")
            {
                CreateSendRecv();
            }
        };
        SendFilter.Filters.CollectionChanged += (s, e) => { CreateSendRecv(); };
        RecvFilter.Filters.CollectionChanged += (s, e) => { CreateSendRecv(); };
    }

    /// <summary>
    /// Gets the send filter model.
    /// </summary>
    public PacketLogFilterViewModel SendFilter { get; }

    /// <summary>
    /// Gets the receive filter model.
    /// </summary>
    public PacketLogFilterViewModel 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; }

    /// <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 or sets whether the recv filter is selected.
    /// </summary>
    public bool RecvFilterSelected { get; set; }

    /// <summary>
    /// Gets or sets whether the send filter is selected.
    /// </summary>
    public bool SendFilterSelected { get; set; }

    private void CreateSendRecv()
    {
        IFilter recvFilter = CreateCompound(RecvFilter);
        IFilter sendFilter = CreateCompound(SendFilter);

        CurrentFilter = new SendRecvFilter(sendFilter, recvFilter);
    }

    private IFilter CreateCompound(PacketLogFilterViewModel packetLogFilter)
    {
        if (!packetLogFilter.Active)
        {
            return new CompoundFilter(true);
        }

        List<IFilter> filters = new List<IFilter>();

        foreach (var filter in packetLogFilter.Filters)
        {
            filters.Add(FilterCreator.BuildFilter(filter.Type, filter.Value));
        }

        return new CompoundFilter(!packetLogFilter.Whitelist, filters.ToArray());
    }

    /// <inheritdoc />
    public void Dispose()
    {
        TogglePane.Dispose();
        CopyPackets.Dispose();
        Clear.Dispose();
        Provider.Dispose();
        (Provider as CommsPacketProvider)?.CustomDispose();
        _cleanUp.Dispose();

        SendFilter.Dispose();
        RecvFilter.Dispose();
    }
}
\ No newline at end of file

R src/PacketLogger/ViewModels/PacketSendSubViewModel.cs => src/PacketLogger/ViewModels/Sender/PacketSendSubViewModel.cs +1 -1
@@ 13,7 13,7 @@ using NosSmooth.PacketSerializer.Abstractions.Attributes;
using PacketLogger.Models.Packets;
using ReactiveUI;

namespace PacketLogger.ViewModels;
namespace PacketLogger.ViewModels.Sender;

/// <inheritdoc />
public class PacketSendSubViewModel : ViewModelBase, IDisposable

R src/PacketLogger/ViewModels/PacketSenderViewModel.cs => src/PacketLogger/ViewModels/Sender/PacketSenderViewModel.cs +1 -3
@@ 5,12 5,10 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Reactive;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using PacketLogger.Models.Packets;
using ReactiveUI;

namespace PacketLogger.ViewModels;
namespace PacketLogger.ViewModels.Sender;

/// <inheritdoc />
public class PacketSenderViewModel : ViewModelBase, IDisposable

A src/PacketLogger/ViewModels/Settings/FilterSettingsViewModel.cs => src/PacketLogger/ViewModels/Settings/FilterSettingsViewModel.cs +89 -0
@@ 0,0 1,89 @@
//
//  FilterSettingsViewModel.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.Collections.ObjectModel;
using System.Reactive;
using PacketLogger.Models.Filters;
using PacketLogger.ViewModels.Filters;
using ReactiveUI;

namespace PacketLogger.ViewModels.Settings;

/// <inheritdoc />
public class FilterSettingsViewModel : SettingViewModelBase
{
    private FilterProfile _currentFilterProfile = null!;

    /// <summary>
    /// Initializes a new instance of the <see cref="FilterSettingsViewModel"/> class.
    /// </summary>
    /// <param name="filterProfiles">The filter profiles.</param>
    public FilterSettingsViewModel(FilterProfiles filterProfiles)
    {
        Profiles = filterProfiles;
        CurrentFilterProfile = Profiles.DefaultProfile;

        AddProfile = ReactiveCommand.Create(
            () =>
            {
                var profile = new FilterProfile(false)
                {
                    Name = "New profile"
                };

                Profiles.AddProfile(profile);
            });

        RemoveCurrentProfile = ReactiveCommand.Create
        (
            () =>
            {
                var currentFilterProfile = CurrentFilterProfile;
                if (currentFilterProfile != Profiles.DefaultProfile)
                {
                    CurrentFilterProfile = Profiles.DefaultProfile;
                    Profiles.RemoveProfile(currentFilterProfile);
                }
            }
        );
    }

    /// <summary>
    /// Gets command for adding a profile.
    /// </summary>
    public ReactiveCommand<Unit, Unit> AddProfile { get; }

    /// <summary>
    /// Gets command for removing the currently selected profile.
    /// </summary>
    public ReactiveCommand<Unit, Unit> RemoveCurrentProfile { get; }

    /// <inheritdoc />
    public override string Name => "Filters";

    /// <summary>
    /// Gets the filter profiles.
    /// </summary>
    public FilterProfiles Profiles { get; }

    /// <summary>
    /// Gets the current filter profile.
    /// </summary>
    public FilterProfile CurrentFilterProfile
    {
        get => _currentFilterProfile;
        set
        {
            _currentFilterProfile = value;
            CurrentFilterProfileViewModel = new FilterConfigViewModel(_currentFilterProfile);
        }
    }

    /// <summary>
    /// Gets the current filter profile view model.
    /// </summary>
    public FilterConfigViewModel CurrentFilterProfileViewModel { get; private set; } = null!;
}
\ No newline at end of file

A src/PacketLogger/ViewModels/Settings/SettingViewModelBase.cs => src/PacketLogger/ViewModels/Settings/SettingViewModelBase.cs +18 -0
@@ 0,0 1,18 @@
//
//  SettingViewModelBase.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.

namespace PacketLogger.ViewModels.Settings;

/// <summary>
/// A base viewmodel class for every settings tab.
/// </summary>
public abstract class SettingViewModelBase : ViewModelBase
{
    /// <summary>
    /// Gets the name of the settings tab.
    /// </summary>
    public abstract string Name { get; }
}
\ No newline at end of file

A src/PacketLogger/ViewModels/Settings/SettingsViewModel.cs => src/PacketLogger/ViewModels/Settings/SettingsViewModel.cs +42 -0
@@ 0,0 1,42 @@
//
//  SettingsViewModel.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.Collections.Generic;
using System.Linq;
using PacketLogger.Models.Filters;

namespace PacketLogger.ViewModels.Settings;

/// <inheritdoc />
public class SettingsViewModel : ViewModelBase
{
    private readonly FilterProfiles _filterProfiles;

    /// <summary>
    /// Initializes a new instance of the <see cref="SettingsViewModel"/> class.
    /// </summary>
    /// <param name="filterProfiles">The filter profiles.</param>
    public SettingsViewModel(FilterProfiles filterProfiles)
    {
        _filterProfiles = filterProfiles;
        Settings = new[]
        {
            new FilterSettingsViewModel(filterProfiles)
        };

        SelectedSetting = Settings.First();
    }

    /// <summary>
    /// Gets the setting tabs list.
    /// </summary>
    public IReadOnlyList<SettingViewModelBase> Settings { get; }

    /// <summary>
    /// Gets the currently selected tab.
    /// </summary>
    public SettingViewModelBase SelectedSetting { get; }
}
\ No newline at end of file

A src/PacketLogger/Views/Filters/FilterChooseView.axaml => src/PacketLogger/Views/Filters/FilterChooseView.axaml +25 -0
@@ 0,0 1,25 @@
<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:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
             xmlns:filters="clr-namespace:PacketLogger.ViewModels.Filters"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.Filters.FilterChooseView">
    <Design.DataContext>
        <filters:FilterChooseViewModel />
    </Design.DataContext>
    <Grid RowDefinitions="45,*">
        <Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="24, Auto, *">
            <TabStrip Grid.Column="2" VerticalAlignment="Center">
                <TabStripItem IsSelected="{Binding RecvFilterSelected, Mode = TwoWay}">Recv</TabStripItem>
                <TabStripItem IsSelected="{Binding SendFilterSelected, Mode = TwoWay}">Send</TabStripItem>
            </TabStrip>
        </Grid>

        <Panel Grid.Row="1" Margin="0,5,0,0">
            <ContentControl IsVisible="{Binding RecvFilterSelected}" Content="{Binding RecvEntryViewModel}"></ContentControl>
            <ContentControl IsVisible="{Binding SendFilterSelected}" Content="{Binding SendEntryViewModel}"></ContentControl>
        </Panel>
    </Grid>
</UserControl>

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

namespace PacketLogger.Views.Filters;

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

    private void InitializeComponent()
    {
        AvaloniaXamlLoader.Load(this);
    }
}
\ No newline at end of file

A src/PacketLogger/Views/Filters/FilterConfigView.axaml => src/PacketLogger/Views/Filters/FilterConfigView.axaml +22 -0
@@ 0,0 1,22 @@
<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:filters="clr-namespace:PacketLogger.ViewModels.Filters"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.Filters.FilterConfigView">
    <Design.DataContext>
        <filters:FilterConfigViewModel />
    </Design.DataContext>
    <Grid RowDefinitions="Auto,Auto,*" ColumnDefinitions="Auto,*,*">
        <TextBlock VerticalAlignment="Center" FontSize="20" Grid.Row="0" Grid.Column="0" Text="Filter Name: " />
        <TextBox Grid.Row="0" Grid.Column="1" IsEnabled="{Binding !Profile.IsDefault}" Text="{Binding Profile.Name}" />

        <TextBlock Margin="0,10,0,0" FontSize="20" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="Recv" />
        <ContentControl Margin="10" VerticalAlignment="Stretch" MaxWidth="300" Grid.Row="2" Grid.Column="0"
                        Grid.ColumnSpan="2" Content="{Binding RecvEntryViewModel}" />
        <TextBlock Margin="0,10,0,0" FontSize="20" Grid.Row="1" Grid.Column="2" Text="Send" />
        <ContentControl Margin="10" VerticalAlignment="Stretch" MaxWidth="300" Grid.Row="2" Grid.Column="2"
                        Content="{Binding SendEntryViewModel}" />
    </Grid>
</UserControl>
\ No newline at end of file

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

namespace PacketLogger.Views.Filters;

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

    private void InitializeComponent()
    {
        AvaloniaXamlLoader.Load(this);
    }
}
\ No newline at end of file

R src/PacketLogger/Views/PacketLogFilterView.axaml => src/PacketLogger/Views/Filters/FilterEntryView.axaml +8 -8
@@ 2,15 2,15 @@
             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.PacketLogFilterView">
             xmlns:filters="clr-namespace:PacketLogger.ViewModels.Filters"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.Filters.FilterEntryView">
    <Design.DataContext>
        <vm:PacketLogFilterViewModel />
        <filters:FilterEntryViewModel />
    </Design.DataContext>

    <Grid ColumnDefinitions="*,*" RowDefinitions="*, 40, 40, 40">
        <DataGrid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Items="{Binding Filters}" IsReadOnly="True"
        <DataGrid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Items="{Binding Entry.Filters}" IsReadOnly="True"
                  SelectedItem="{Binding SelectedFilter}"
                  CanUserReorderColumns="True" CanUserSortColumns="True" CanUserResizeColumns="True">
            <DataGrid.Styles>


@@ 24,11 24,11 @@
            </DataGrid.Columns>
        </DataGrid>
        
        <CheckBox Grid.Row="1" Grid.Column="0" Content="Filter active" IsChecked="{Binding Active}" />
        <CheckBox Grid.Row="1" Grid.Column="0" Content="Filter active" IsChecked="{Binding Entry.Active}" />

        <Grid Grid.Row="1" Grid.Column="1" ColumnDefinitions="*,*">
            <RadioButton Grid.Column="0" Content="Wl" GroupName="WlBl"></RadioButton>
            <RadioButton Grid.Column="1" Content="Bl" GroupName="WlBl" IsChecked="{Binding !Whitelist}"></RadioButton>
            <RadioButton Grid.Column="1" Content="Bl" GroupName="WlBl" IsChecked="{Binding !Entry.Whitelist}"></RadioButton>
        </Grid>

        <TextBox VerticalAlignment="Center" Margin="0, 0, 5, 0" Height="30" Grid.Row="2" Grid.Column="0"


@@ 43,4 43,4 @@
        <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
</UserControl>

R src/PacketLogger/Views/PacketLogFilterView.axaml.cs => src/PacketLogger/Views/Filters/FilterEntryView.axaml.cs +5 -5
@@ 1,5 1,5 @@
//
//  PacketLogFilterView.axaml.cs
//  FilterEntryView.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.


@@ 11,15 11,15 @@ using Avalonia.Markup.Xaml;
using PacketLogger.Models.Filters;
using PropertyChanged;

namespace PacketLogger.Views;
namespace PacketLogger.Views.Filters;

[DoNotNotify]
public partial class PacketLogFilterView : UserControl
public partial class FilterEntryView : UserControl
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketLogFilterView"/> class.
    /// Initializes a new instance of the <see cref="FilterEntryView"/> class.
    /// </summary>
    public PacketLogFilterView()
    public FilterEntryView()
    {
        InitializeComponent();
        this.FindControl<ComboBox>("FilterType").Items = Enum.GetValues<FilterCreator.FilterType>();

M src/PacketLogger/Views/MainWindow.axaml => src/PacketLogger/Views/MainWindow.axaml +1 -0
@@ 40,6 40,7 @@
                <MenuItem Header="_Save Filtered As..." Command="{Binding SaveFiltered}" />
                <MenuItem Header="Save All As..." Command="{Binding SaveAll}" />
                <Separator />
                <MenuItem Header="Open Settings" Command="{Binding OpenSettings}" />
                <MenuItem Header="Exit" Command="{Binding QuitApplication}" />
            </MenuItem>
            <MenuItem Header="_Tools">

D src/PacketLogger/Views/PacketLogView.axaml => src/PacketLogger/Views/PacketLogView.axaml +0 -98
@@ 1,98 0,0 @@
<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.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"
             x:Name="UserControl">
    <UserControl.Resources>
        <converters:PacketSourceConverter x:Key="packetSourceConverter" />
    </UserControl.Resources>
    <Design.DataContext>
        <vm:PacketLogViewModel />
    </Design.DataContext>
    <SplitView OpenPaneLength="300" IsPaneOpen="{Binding PaneOpen, Mode = TwoWay}" DisplayMode="CompactInline"
               PanePlacement="Right">
        <SplitView.Pane>
            <Grid Width="280" HorizontalAlignment="Left"
                  ColumnDefinitions="*" RowDefinitions="*,80" Margin="10">
                <Grid Grid.Row="0" RowDefinitions="45,*">
                    <Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="24, Auto, *">
                        <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" />
                        <TabStrip Grid.Column="2" VerticalAlignment="Center">
                            <TabStripItem IsSelected="{Binding RecvFilterSelected, Mode = TwoWay}">Recv</TabStripItem>
                            <TabStripItem IsSelected="{Binding SendFilterSelected, Mode = TwoWay}">Send</TabStripItem>
                        </TabStrip>
                    </Grid>

                    <Panel Grid.Row="1" Margin="0,5,0,0">
                        <ContentControl IsVisible="{Binding RecvFilterSelected}" Content="{Binding RecvFilter}"></ContentControl>
                        <ContentControl IsVisible="{Binding SendFilterSelected}" Content="{Binding SendFilter}"></ContentControl>
                    </Panel>
                </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>

        <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

R src/PacketLogger/Views/PacketSendSubView.axaml => src/PacketLogger/Views/Sender/PacketSendSubView.axaml +3 -2
@@ 4,13 4,14 @@
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:viewModels="clr-namespace:PacketLogger.ViewModels"
             xmlns:converters="clr-namespace:PacketLogger.Converters"
             xmlns:sender="clr-namespace:PacketLogger.ViewModels.Sender"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.PacketSendSubView">
             x:Class="PacketLogger.Views.Sender.PacketSendSubView">
    <UserControl.Resources>
        <converters:PacketSourceConverter x:Key="packetSourceConverter" />
    </UserControl.Resources>
    <Design.DataContext>
        <viewModels:PacketSendSubViewModel />
        <sender:PacketSendSubViewModel />
    </Design.DataContext>

    <DockPanel>

R src/PacketLogger/Views/PacketSendSubView.axaml.cs => src/PacketLogger/Views/Sender/PacketSendSubView.axaml.cs +1 -2
@@ 4,12 4,11 @@
//  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;
namespace PacketLogger.Views.Sender;

[DoNotNotify]
public partial class PacketSendSubView : UserControl

R src/PacketLogger/Views/PacketSenderView.axaml => src/PacketLogger/Views/Sender/PacketSenderView.axaml +3 -2
@@ 3,10 3,11 @@
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:viewModels="clr-namespace:PacketLogger.ViewModels"
             xmlns:sender="clr-namespace:PacketLogger.ViewModels.Sender"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.PacketSenderView">
             x:Class="PacketLogger.Views.Sender.PacketSenderView">
    <Design.DataContext>
        <viewModels:PacketSenderViewModel />
        <sender:PacketSenderViewModel />
    </Design.DataContext>
    <Grid RowDefinitions="15,*,15,*" Margin="10">
        <TextBlock Grid.Row="0" Text="Recv" Margin="0,0,0,5" />

R src/PacketLogger/Views/PacketSenderView.axaml.cs => src/PacketLogger/Views/Sender/PacketSenderView.axaml.cs +1 -2
@@ 4,12 4,11 @@
//  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;
namespace PacketLogger.Views.Sender;

[DoNotNotify]
public partial class PacketSenderView : UserControl

A src/PacketLogger/Views/Settings/FilterSettingsView.axaml => src/PacketLogger/Views/Settings/FilterSettingsView.axaml +30 -0
@@ 0,0 1,30 @@
<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:settings="clr-namespace:PacketLogger.ViewModels.Settings"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.Settings.FilterSettingsView">
    <Design.DataContext>
        <settings:FilterSettingsViewModel />
    </Design.DataContext>
    <StackPanel Orientation="Horizontal">
        <Grid RowDefinitions="*, Auto" ColumnDefinitions="*,*">
            <ListBox Margin="0,0,0,10" Grid.ColumnSpan="2" Width="150" Items="{Binding Profiles.AllProfiles}" SelectedItem="{Binding CurrentFilterProfile}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Name}" />
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
            
            <Button Margin="0,0,5,0" Grid.Row="1" Grid.Column="0" Content="Remove" Command="{Binding RemoveCurrentProfile}" />
            <Button Margin="5,0,0,0" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" Content="Add" Command="{Binding AddProfile}" />
        </Grid>

        <Grid Margin="10,0,0,0" RowDefinitions="Auto,*" VerticalAlignment="Stretch">
            <CheckBox Grid.Row="0" Content="Enable default filter" IsChecked="{Binding Profiles.DefaultFilterEnabled}" />
            <ContentControl Grid.Row="1" VerticalAlignment="Stretch" Margin="0,10,0,0" Content="{Binding CurrentFilterProfileViewModel}" />
        </Grid>
    </StackPanel>
</UserControl>
\ No newline at end of file

R src/PacketLogger/Views/PacketLogView.axaml.cs => src/PacketLogger/Views/Settings/FilterSettingsView.axaml.cs +6 -15
@@ 1,24 1,23 @@
//
//  PacketLogView.axaml.cs
//
//  FilterSettingsView.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;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PropertyChanged;

namespace PacketLogger.Views;
namespace PacketLogger.Views.Settings;

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


@@ 27,12 26,4 @@ public partial class PacketLogView : UserControl
    {
        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

A src/PacketLogger/Views/Settings/SettingsView.axaml => src/PacketLogger/Views/Settings/SettingsView.axaml +22 -0
@@ 0,0 1,22 @@
<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:settings="clr-namespace:PacketLogger.ViewModels.Settings"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.Settings.SettingsView">
    <Design.DataContext>
        <settings:SettingsViewModel />
    </Design.DataContext>
    <StackPanel Orientation="Horizontal" Margin="10">
        <ListBox Margin="10" Width="100" Items="{Binding Settings}" SelectedItem="{Binding SelectedSetting}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"></TextBlock>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        
        <ContentControl VerticalAlignment="Stretch" Margin="10" Content="{Binding SelectedSetting}" />
    </StackPanel>
</UserControl>

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

namespace PacketLogger.Views.Settings;

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

    private void InitializeComponent()
    {
        AvaloniaXamlLoader.Load(this);
    }
}
\ No newline at end of file

Do not follow this link