~ruther/NosTale-PacketLogger

e12f03d75b98f3416048b612a8bb54bb904e6551 — Rutherther 2 years ago 3d61d60
feat: add title generator that reacts to new tabs with same title
M src/PacketLogger/Models/Packets/ClientPacketProvider.cs => src/PacketLogger/Models/Packets/ClientPacketProvider.cs +5 -0
@@ 54,6 54,11 @@ public abstract class ClientPacketProvider : ReactiveObject, IPacketProvider
        ? _process.BrowserManager.PlayerManager.Get().Player.Name
        : null) ?? $"Not in game ({_process.Process.Id})";

    /// <summary>
    /// Gets or sets title of document.
    /// </summary>
    public string DocumentTitle { get; set; } = string.Empty;

    /// <inheritdoc />
    public abstract bool IsOpen { get; }


M src/PacketLogger/Models/Packets/DummyPacketProvider.cs => src/PacketLogger/Models/Packets/DummyPacketProvider.cs +5 -0
@@ 38,6 38,11 @@ public class DummyPacketProvider : IPacketProvider, IDisposable
    /// <inheritdoc />
    public event PropertyChangedEventHandler? PropertyChanged;

    /// <summary>
    /// Gets or sets title of document.
    /// </summary>
    public string DocumentTitle { get; set; } = string.Empty;

    /// <inheritdoc />
    public bool LogReceived
    {

M src/PacketLogger/Models/Packets/FilePacketProvider.cs => src/PacketLogger/Models/Packets/FilePacketProvider.cs +5 -0
@@ 39,6 39,11 @@ public class FilePacketProvider : IPacketProvider
        _fileName = fileName;
    }

    /// <summary>
    /// Gets or sets title of document.
    /// </summary>
    public string DocumentTitle { get; set; } = string.Empty;

    /// <inheritdoc />
    public string Name => Path.GetFileName(_fileName);


M src/PacketLogger/Models/Packets/IPacketProvider.cs => src/PacketLogger/Models/Packets/IPacketProvider.cs +6 -0
@@ 12,6 12,7 @@ using System.Threading;
using System.Threading.Tasks;
using DynamicData;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Titles;
using Remora.Results;

namespace PacketLogger.Models.Packets;


@@ 27,6 28,11 @@ public interface IPacketProvider : INotifyPropertyChanged, IDisposable
    public string Name { get; }

    /// <summary>
    /// Gets title of document, if any.
    /// </summary>
    public string DocumentTitle { get; set; }

    /// <summary>
    /// Gets whether <see cref="Open"/> was called and successfully finished.
    /// </summary>
    public bool IsOpen { get; }

A src/PacketLogger/Models/Titles/NumberedTitleGenerator.cs => src/PacketLogger/Models/Titles/NumberedTitleGenerator.cs +180 -0
@@ 0,0 1,180 @@
//
//  NumberedTitleGenerator.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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;

namespace PacketLogger.Models.Titles;

/// <summary>
/// A generator and manager of document titles.
/// </summary>
public class NumberedTitleGenerator
{
    private readonly SemaphoreSlim _semaphore;
    private readonly ConcurrentDictionary<string, List<Title>> _titles;

    /// <summary>
    /// Initializes a new instance of the <see cref="NumberedTitleGenerator"/> class.
    /// </summary>
    public NumberedTitleGenerator()
    {
        _semaphore = new SemaphoreSlim(1, 1);
        _titles = new ConcurrentDictionary<string, List<Title>>();
    }

    /// <summary>
    /// Add title with given information.
    /// </summary>
    /// <param name="setDocumentTitle">The function used for setting new document title.</param>
    /// <param name="titleChanged">The observable observing changes to the default, unnumbered title.</param>
    /// <param name="initialTitle">The current initial title.</param>
    /// <returns>The handle, title will be removed upon disposal.</returns>
    public TitleHandle AddTitle
    (
        Action<string> setDocumentTitle,
        IObservable<string> titleChanged,
        string initialTitle
    )
    {
        var title = new Title
        (
            setDocumentTitle,
            initialTitle
        );

        title.TitleChanged = titleChanged.Subscribe
        (
            newTitle => HandleTitleChange(title, newTitle)
        );
        HandleTitleChange(title, title.CurrentTitle);

        return new TitleHandle(this, title);
    }

    private void RemoveTitle(Title title)
    {
        _titles.AddOrUpdate
        (
            title.CurrentTitle,
            _ => new List<Title>(),
            (_, u) =>
            {
                u.Remove(title);
                return u;
            }
        );
        UpdateNumbers(title.CurrentTitle);
    }

    private void HandleTitleChange(Title title, string newTitle)
    {
        _semaphore.Wait();
        _titles.AddOrUpdate
        (
            title.CurrentTitle,
            _ => new List<Title>(),
            (_, u) =>
            {
                u.Remove(title);
                return u;
            }
        );
        UpdateNumbers(title.CurrentTitle);

        title.CurrentTitle = newTitle;
        _titles.TryAdd(newTitle, new List<Title>());
        _titles.AddOrUpdate
        (
            newTitle,
            _ => new List<Title>(),
            (_, u) =>
            {
                u.Add(title);
                return u;
            }
        );
        UpdateNumbers(title.CurrentTitle);
        _semaphore.Release();
    }

    private void UpdateNumbers(string title)
    {
        if (_titles.TryGetValue(title, out var titles))
        {
            if (titles.Count == 1)
            {
                titles[0].CurrentNumber = 0;
                titles[0].SetDocumentTitle(titles[0].CurrentTitle);
            }
            else if (titles.Count > 1)
            {
                titles[0].CurrentNumber = null;
                titles[0].SetDocumentTitle(titles[0].CurrentTitle);

                for (int i = 1; i < titles.Count; i++)
                {
                    titles[i].CurrentNumber = i;
                    titles[i].SetDocumentTitle($"{titles[i].CurrentTitle} ({i})");
                }
            }
        }
    }

    /// <summary>
    /// A store of a title.
    /// </summary>
    public class TitleHandle : IDisposable
    {
        private readonly NumberedTitleGenerator _titleGenerator;
        private readonly Title _title;

        /// <summary>
        /// Initializes a new instance of the <see cref="TitleHandle"/> class.
        /// </summary>
        /// <param name="titleGenerator">The title generator.</param>
        /// <param name="title">The title.</param>
        public TitleHandle(NumberedTitleGenerator titleGenerator, Title title)
        {
            _titleGenerator = titleGenerator;
            _title = title;
        }

        /// <inheritdoc />
        public void Dispose()
        {
            _title.TitleChanged.Dispose();
            _titleGenerator.RemoveTitle(_title);
        }
    }

    /// <summary>
    /// A title.
    /// </summary>
    /// <param name="SetDocumentTitle">The function used for setting the title of document.</param>
    /// <param name="CurrentTitle">The current title.</param>
    public record Title(Action<string> SetDocumentTitle, string CurrentTitle)
    {
        /// <summary>
        /// Gets or sets the current suffix.
        /// </summary>
        public int? CurrentNumber { get; set; }

        /// <summary>
        /// Gets or sets the disposable title changed observer.
        /// </summary>
        public IDisposable TitleChanged { get; set; } = null!;

        /// <summary>
        /// Gets or sets the current title.
        /// </summary>
        public string CurrentTitle { get; set; } = CurrentTitle;
    }
}
\ No newline at end of file

M src/PacketLogger/ViewModels/DockFactory.cs => src/PacketLogger/ViewModels/DockFactory.cs +9 -3
@@ 18,6 18,7 @@ using NosSmooth.Core.Stateful;
using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;
using PacketLogger.Models.Titles;
using PacketLogger.Views;
using ReactiveUI;
using HostWindow = PacketLogger.Views.HostWindow;


@@ 35,6 36,7 @@ public class DockFactory : Factory, IDisposable
    private readonly ObservableCollection<IPacketProvider> _providers;
    private readonly NostaleProcesses _processes;
    private readonly CommsInjector _injector;
    private readonly NumberedTitleGenerator _titleGenerator;

    private IRootDock? _rootDock;
    private IDocumentDock? _documentDock;


@@ 58,6 60,7 @@ public class DockFactory : Factory, IDisposable
        StatefulRepository repository
    )
    {
        _titleGenerator = new NumberedTitleGenerator();
        _services = services;
        _filterProfiles = filterProfiles;
        _providers = providers;


@@ 111,10 114,11 @@ public class DockFactory : Factory, IDisposable
                _repository,
                _providers,
                _processes,
                _titleGenerator,
                OnDocumentLoaded,
                OnDocumentClosed
            )
            { Id = $"New tab", Title = $"New tab" };
            { Id = Guid.NewGuid().ToString() };

        var observable = load(document);
        observable.Subscribe


@@ 155,10 159,11 @@ public class DockFactory : Factory, IDisposable
                        _repository,
                        _providers,
                        _processes,
                        _titleGenerator,
                        OnDocumentLoaded,
                        OnDocumentClosed
                    )
                    { Id = $"New tab {index}", Title = $"New tab {index}" };
                    { Id = $"New tab {index}" };

                AddDockable(documentDock, document);
                SetActiveDockable(document);


@@ 179,10 184,11 @@ public class DockFactory : Factory, IDisposable
                _repository,
                _providers,
                _processes,
                _titleGenerator,
                OnDocumentLoaded,
                OnDocumentClosed
            )
            { Id = $"New tab", Title = $"New tab" };
            { Id = $"New tab" };
        var documentDock = CreateDocumentDock();
        documentDock.IsCollapsable = false;
        documentDock.ActiveDockable = initialTab;

M src/PacketLogger/ViewModels/DocumentViewModel.cs => src/PacketLogger/ViewModels/DocumentViewModel.cs +67 -30
@@ 30,6 30,7 @@ using NosSmooth.Pcap;
using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;
using PacketLogger.Models.Titles;
using PacketLogger.ViewModels.Log;
using PacketLogger.ViewModels.Sender;
using PacketLogger.ViewModels.Settings;


@@ 47,6 48,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
    private CancellationTokenSource _ctSource;
    private IPacketProvider? _packetProvider;
    private IDisposable? _cleanUp;
    private NumberedTitleGenerator.TitleHandle _titleHandle;

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


@@ 57,6 59,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
    /// <param name="repository">The repository.</param>
    /// <param name="providers">The providers.</param>
    /// <param name="processes">The NosTale processes collection.</param>
    /// <param name="titleGenerator">The title generator.</param>
    /// <param name="onDocumentLoaded">The action to call on loaded.</param>
    /// <param name="onDocumentUnloaded">The action to call on document unloaded/closed.</param>
    public DocumentViewModel


@@ 67,10 70,28 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
        StatefulRepository repository,
        ObservableCollection<IPacketProvider> providers,
        NostaleProcesses processes,
        NumberedTitleGenerator titleGenerator,
        Action<DocumentViewModel> onDocumentLoaded,
        Action<DocumentViewModel> onDocumentUnloaded
    )
    {
        _titleHandle = titleGenerator.AddTitle
        (
            title => Title = title,
            Observable.Empty<string>(),
            "New tab"
        );

        _cleanUp = this.WhenAnyValue(x => x.Title)
            .Subscribe(title =>
                {
                    if (_packetProvider is not null)
                    {
                        _packetProvider.DocumentTitle = title;
                    }
                }
            );

        FilterProfiles = filterProfiles;
        _ctSource = new CancellationTokenSource();
        _providers = providers;


@@ 84,6 105,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
                {
                    Loading = true;
                    _packetProvider = new DummyPacketProvider(Title);
                    _packetProvider.DocumentTitle = Title;
                    NestedViewModel = new PacketLogViewModel(_packetProvider, filterProfiles);
                    Loaded = true;
                    onDocumentLoaded(this);


@@ 121,7 143,14 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
                    return;
                }

                Title = Path.GetFileName(path);
                _titleHandle?.Dispose();
                _titleHandle = titleGenerator.AddTitle
                (
                    title => Title = title,
                    Observable.Empty<string>(),
                    Path.GetFileName(path)
                );
                _packetProvider.DocumentTitle = Title;
                NestedViewModel = new PacketLogViewModel(provider, filterProfiles);
                Loaded = true;
                Loading = false;


@@ 168,21 197,16 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
                    return;
                }

                _cleanUp = process.WhenPropertyChanged(x => x.CharacterString)
                    .ObserveOn(RxApp.MainThreadScheduler)
                    .Do
                    (
                        _ =>
                        {
                            Title = (process.BrowserManager.IsInGame.Get()
                                ? process.BrowserManager.PlayerManager.Get().Player.Name
                                : null) ?? $"Not in game ({process.Process.Id})";
                        }
                    )
                    .Subscribe();
                _titleHandle?.Dispose();
                _titleHandle = titleGenerator.AddTitle
                (
                    title => Title = title,
                    provider.WhenAnyValue(x => x.Name).ObserveOn(RxApp.MainThreadScheduler),
                    provider.Name
                );

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


@@ 209,23 233,20 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
                _packetProvider = provider;
                repository.SetEntity<ClientPacketProvider>(client, provider);

                _cleanUp = process.WhenPropertyChanged(x => x.CharacterString)
                    .ObserveOn(RxApp.MainThreadScheduler)
                    .Do
                    (
                        _ =>
                        {
                            Title = (process.BrowserManager.IsInGame.Get()
                                ? process.BrowserManager.PlayerManager.Get().Player.Name
                                : null) ?? $"Not in game ({process.Process.Id})";
                        }
                    )
                    .Subscribe();

                _titleHandle?.Dispose();
                _titleHandle = titleGenerator.AddTitle
                (
                    title => Title = title,
                    provider
                        .WhenAnyValue(x => x.Name)
                        .ObserveOn(RxApp.MainThreadScheduler)
                        .Select(x => x + " - sniff"),
                    provider.Name
                );
                await provider.Open();

                _packetProvider.DocumentTitle = Title;
                NestedViewModel = new PacketLogViewModel(provider, filterProfiles);
                Title = provider.Name;
                Loading = false;
                Loaded = true;
                onDocumentLoaded(this);


@@ 238,7 259,16 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
            {
                Loading = true;
                NestedViewModel = new PacketSenderViewModel(provider);
                Title = $"Sender ({provider.Name})";
                _titleHandle?.Dispose();
                _titleHandle = titleGenerator.AddTitle
                (
                    title => Title = title,
                    provider
                        .WhenAnyValue(x => x.DocumentTitle)
                        .ObserveOn(RxApp.MainThreadScheduler)
                        .Select(x => $"Sender - {provider.DocumentTitle}"),
                    provider.Name
                );
                Loaded = true;
                Loading = false;
            }


@@ 250,7 280,13 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
        (
            () =>
            {
                Title = "Settings";
                _titleHandle?.Dispose();
                _titleHandle = titleGenerator.AddTitle
                (
                    title => Title = title,
                    Observable.Empty<string>(),
                    "Settings"
                );
                NestedViewModel = new SettingsViewModel(filterProfiles);
                Loaded = true;
            }


@@ 354,6 390,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
    /// <inheritdoc />
    public void Dispose()
    {
        _titleHandle?.Dispose();
        _cleanUp?.Dispose();
        _ctSource.Cancel();
        _ctSource.Dispose();

M src/PacketLogger/Views/DocumentView.axaml => src/PacketLogger/Views/DocumentView.axaml +2 -2
@@ 12,7 12,7 @@
    </Design.DataContext>

    <Grid>
        <Border Grid.Row="1" IsVisible="{Binding !Loaded}"
        <Border IsVisible="{Binding !Loaded}"
                MaxWidth="1000" MaxHeight="600"
                CornerRadius="25"
                Background="{DynamicResource SystemControlPageBackgroundChromeLowBrush}">


@@ 116,7 116,7 @@
                        <TextBlock FontSize="30" Margin="0,0,0,5" Text="Open a sender for" />
                        <DataGrid Margin="0,0,30,0" Items="{Binding Providers}">
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="Tab" Binding="{Binding Name}" />
                                <DataGridTextColumn Header="Tab" Binding="{Binding DocumentTitle}" />
                                <DataGridTemplateColumn Header="Open">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>

M src/PacketLogger/Views/MainWindow.axaml => src/PacketLogger/Views/MainWindow.axaml +1 -1
@@ 48,7 48,7 @@
                <MenuItem Header="_Packet Sender" Command="{Binding Connect}" Items="{Binding Providers}">
                    <MenuItem.Styles>
                        <Style Selector="MenuItem">
                            <Setter Property="Header" Value="{Binding Name}" />
                            <Setter Property="Header" Value="{Binding DocumentTitle}" />
                            <Setter Property="Command" Value="{Binding OpenSender}" />
                            <Setter Property="CommandParameter"
                                    Value="{Binding SelectedItems, RelativeSource={RelativeSource Self}}" />

Do not follow this link