Log view can copy logs via the copy button

This commit is contained in:
Timothy Baldridge 2021-10-18 21:37:10 -06:00
parent 7163f1c8f0
commit b134ff9fc5
12 changed files with 176 additions and 73 deletions

View File

@ -8,5 +8,7 @@ namespace Wabbajack.App
public AbsolutePath SavedSettingsLocation { get; set; }
public AbsolutePath EncryptedDataLocation { get; set; }
public AbsolutePath LogLocation { get; set; }
}
}

View File

@ -3,20 +3,33 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Wabbajack.App.Controls"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Wabbajack.App.Controls.LogView">
<ScrollViewer ScrollChanged="ScrollViewer_OnScrollChanged" x:Name="ScrollViewer">
<ItemsControl x:Name="Messages">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:LogViewItem></controls:LogViewItem>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Grid RowDefinitions="Auto, *, Auto">
<TextBlock Grid.Row="0">Current Log Contents</TextBlock>
<ScrollViewer Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged" x:Name="ScrollViewer"
HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
<ItemsControl x:Name="Messages">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:LogViewItem></controls:LogViewItem>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="CopyLog">
<avalonia:MaterialIcon Kind="ContentCopy"></avalonia:MaterialIcon>
</Button>
<Button x:Name="OpenFolder">
<avalonia:MaterialIcon Kind="Folder"></avalonia:MaterialIcon>
</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@ -17,6 +17,8 @@ public partial class LogView : ReactiveUserControl<LogViewModel>
{
this.OneWayBind(ViewModel, vm => vm.Messages, view => view.Messages.Items)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CopyLogFile, view => view.CopyLog)
.DisposeWith(disposables);
});
}

View File

@ -1,9 +1,15 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive;
using System.Reactive.Disposables;
using Avalonia;
using Avalonia.Controls.Mixins;
using Avalonia.Input;
using DynamicData;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.App.Utilities;
using Wabbajack.App.ViewModels;
@ -12,26 +18,21 @@ namespace Wabbajack.App.Controls;
public class LogViewModel : ViewModelBase, IActivatableViewModel
{
private readonly LoggerProvider _provider;
private readonly SourceCache<LoggerProvider.ILogMessage, long> _messages;
public readonly ReadOnlyObservableCollection<LoggerProvider.ILogMessage> _messagesFiltered;
public ReadOnlyObservableCollection<LoggerProvider.ILogMessage> Messages => _messagesFiltered;
public ReadOnlyObservableCollection<LoggerProvider.ILogMessage> Messages => _provider.MessageLog;
[Reactive]
public ReactiveCommand<Unit, Unit> CopyLogFile { get; set; }
public LogViewModel(LoggerProvider provider)
{
_messages = new SourceCache<LoggerProvider.ILogMessage, long>(m => m.MessageId);
//_messages.LimitSizeTo(100);
Activator = new ViewModelActivator();
_provider = provider;
_messages.Connect()
.Bind(out _messagesFiltered)
.Subscribe();
_provider.Messages
.Subscribe(m => _messages.AddOrUpdate(m));
CopyLogFile = ReactiveCommand.Create(() =>
{
var obj = new DataObject();
obj.Set(DataFormats.FileNames, new List<string> {_provider.LogPath.ToString()});
Application.Current.Clipboard.SetDataObjectAsync(obj);
});
}
}

View File

@ -5,33 +5,7 @@
xmlns:controls="clr-namespace:Wabbajack.App.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Wabbajack.App.Screens.LogScreenView">
<Grid RowDefinitions="*, Auto">
<Viewbox Grid.Row="0"
Name="Banner"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="UniformToFill">
<Image x:Name="ModListImage" Margin="0,0,0,0" Source="../Assets/Wabba_Mouth.png" />
</Viewbox>
<Grid Grid.Row="0" RowDefinitions="40, 40" HorizontalAlignment="Left" VerticalAlignment="Bottom">
<TextBlock x:Name="ModListName"></TextBlock>
</Grid>
<Grid Margin="40" RowDefinitions="40, 40, 40, *" ColumnDefinitions="100, *, 200" Grid.Row="1">
<Label Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right" VerticalAlignment="Center">ModList:</Label>
<TextBox Grid.Column="1" Grid.Row="0" IsEnabled="False" Height="20" x:Name="ModList"></TextBox>
<Label Grid.Column="0" Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Center">Location:</Label>
<TextBox Grid.Column="1" Grid.Row="1" IsEnabled="False" Height="20" x:Name="InstallPath"></TextBox>
<Grid Grid.Column="1" Grid.Row="3" Grid.ColumnDefinitions="*, *, *" HorizontalAlignment="Center">
<Button Grid.Column="0" x:Name="WebsiteButton">Website</Button>
<Button Grid.Column="1" x:Name="ReadmeButton">Readme</Button>
<Button Grid.Column="2" x:Name="LocalFilesButton">Local Files</Button>
</Grid>
<controls:LargeIconButton x:Name="PlayGame" Margin="40, 0, 0, 0" Grid.Row="0" Grid.Column="2" Grid.RowSpan="4" Icon="PlayCircle" Text="Play">
</controls:LargeIconButton>
</Grid>
<Grid RowDefinitions="*">
<controls:LogView></controls:LogView>
</Grid>
</UserControl>

View File

@ -1,8 +1,12 @@
using ReactiveUI;
using Wabbajack.App.Views;
namespace Wabbajack.App.Screens;
public partial class LogScreenView : ScreenBase<LogScreenViewModel>
{
public LogScreenView()
{
InitializeComponent();
}
}

View File

@ -1,9 +1,21 @@
using System.Reactive;
using Avalonia;
using Avalonia.Input;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.App.Utilities;
using Wabbajack.App.ViewModels;
namespace Wabbajack.App.Screens;
public class LogScreenViewModel : ViewModelBase, IActivatableViewModel
{
private readonly LoggerProvider _provider;
public LogScreenViewModel(LoggerProvider provider)
{
_provider = provider;
Activator = new ViewModelActivator();
}
}

View File

@ -45,6 +45,7 @@ namespace Wabbajack.App
services.AddDTOSerializer();
services.AddSingleton<ModeSelectionViewModel>();
services.AddTransient<FileSelectionBoxViewModel>();
services.AddSingleton<IScreenView, LogScreenView>();
services.AddSingleton<IScreenView, ModeSelectionView>();
services.AddSingleton<IScreenView, InstallConfigurationView>();
services.AddSingleton<IScreenView, CompilerConfigurationView>();
@ -57,6 +58,7 @@ namespace Wabbajack.App
services.AddSingleton<InstallationStateManager>();
services.AddSingleton<HttpClient>();
services.AddSingleton<LogScreenViewModel>();
services.AddAllSingleton<IReceiverMarker, StandardInstallationViewModel>();
services.AddAllSingleton<IReceiverMarker, InstallConfigurationViewModel>();
services.AddAllSingleton<IReceiverMarker, CompilerConfigurationViewModel>();
@ -89,7 +91,8 @@ namespace Wabbajack.App
{
EncryptedDataLocation = KnownFolders.WabbajackAppLocal.Combine("encrypted"),
ModListsDownloadLocation = KnownFolders.EntryPoint.Combine("downloaded_mod_lists"),
SavedSettingsLocation = KnownFolders.WabbajackAppLocal.Combine("saved_settings")
SavedSettingsLocation = KnownFolders.WabbajackAppLocal.Combine("saved_settings"),
LogLocation = KnownFolders.EntryPoint.Combine("logs")
});
services.AddSingleton<SettingsManager>();

View File

@ -1,9 +1,16 @@
using System;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using System.Text;
using System.Threading;
using DynamicData;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack.App.Utilities;
@ -12,16 +19,67 @@ public class LoggerProvider : ILoggerProvider
private Subject<ILogMessage> _messages = new();
public IObservable<ILogMessage> Messages => _messages;
private long _messageID = 0;
private long _messageId = 0;
private SourceCache<ILogMessage, long> _messageLog = new(m => m.MessageId);
public long NextMessageId()
public readonly ReadOnlyObservableCollection<ILogMessage> _messagesFiltered;
private readonly CompositeDisposable _disposables;
private readonly Configuration _configuration;
private readonly DateTime _startupTime;
private readonly RelativePath _appName;
public AbsolutePath LogPath { get; }
private readonly Stream _logFile;
private readonly StreamWriter _logStream;
public ReadOnlyObservableCollection<ILogMessage> MessageLog => _messagesFiltered;
public LoggerProvider(Configuration configuration)
{
return Interlocked.Increment(ref _messageID);
_startupTime = DateTime.UtcNow;
_configuration = configuration;
_configuration.LogLocation.CreateDirectory();
_disposables = new CompositeDisposable();
Messages.Subscribe(m => _messageLog.AddOrUpdate(m))
.DisposeWith(_disposables);
Messages.Subscribe(m => LogToFile(m))
.DisposeWith(_disposables);
_messageLog.Connect()
.Bind(out _messagesFiltered)
.Subscribe()
.DisposeWith(_disposables);
_messages.DisposeWith(_disposables);
_appName = typeof(LoggerProvider).Assembly.Location.ToAbsolutePath().FileName;
LogPath = _configuration.LogLocation.Combine($"{_appName}.current.log");
_logFile = LogPath.Open(FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
_logFile.DisposeWith(_disposables);
_logStream = new StreamWriter(_logFile, Encoding.UTF8);
}
private void LogToFile(ILogMessage logMessage)
{
var line = $"[{logMessage.TimeStamp - _startupTime}] {logMessage.LongMessage}";
lock (_logStream)
{
_logStream.Write(line);
_logStream.Flush();
}
}
private long NextMessageId()
{
return Interlocked.Increment(ref _messageId);
}
public void Dispose()
{
_messages.Dispose();
_disposables.Dispose();
}
public ILogger CreateLogger(string categoryName)
@ -43,7 +101,7 @@ public class LoggerProvider : ILoggerProvider
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
_provider._messages.OnNext(new LogMessage<TState>(_provider.NextMessageId(), logLevel, eventId, state, exception, formatter));
_provider._messages.OnNext(new LogMessage<TState>(DateTime.UtcNow, _provider.NextMessageId(), logLevel, eventId, state, exception, formatter));
}
public bool IsEnabled(LogLevel logLevel)
@ -63,10 +121,28 @@ public class LoggerProvider : ILoggerProvider
long MessageId { get; }
string ShortMessage { get; }
DateTime TimeStamp { get; }
string LongMessage { get; }
}
record LogMessage<TState>(long MessageId, LogLevel LogLevel, EventId EventId, TState State, Exception? Exception, Func<TState, Exception?, string> Formatter) : ILogMessage
record LogMessage<TState>(DateTime TimeStamp, long MessageId, LogLevel LogLevel, EventId EventId, TState State, Exception? Exception, Func<TState, Exception?, string> Formatter) : ILogMessage
{
public string ShortMessage => Formatter(State, Exception);
public string LongMessage
{
get
{
var sb = new StringBuilder();
sb.AppendLine(ShortMessage);
if (Exception != null)
{
sb.Append("Exception: ");
sb.Append(Exception);
}
return sb.ToString();
}
}
}
}

View File

@ -43,6 +43,9 @@ namespace Wabbajack.App.ViewModels
[Reactive]
public ReactiveCommand<Unit, Unit> SettingsButton { get; set; }
[Reactive]
public ReactiveCommand<Unit, Unit> LogViewButton { get; set; }
[Reactive]
public string ResourceStatus { get; set; }
@ -72,9 +75,15 @@ namespace Wabbajack.App.ViewModels
.DisposeWith(disposables);
SettingsButton = ReactiveCommand.Create(() =>
{
Receive(new NavigateTo(typeof(SettingsViewModel)));
})
{
Receive(new NavigateTo(typeof(SettingsViewModel)));
})
.DisposeWith(disposables);
LogViewButton = ReactiveCommand.Create(() =>
{
Receive(new NavigateTo(typeof(LogScreenViewModel)));
})
.DisposeWith(disposables);
});

View File

@ -34,21 +34,25 @@
</Design.DataContext>
<Grid RowDefinitions="40, *">
<Grid ColumnDefinitions="40, *, 40, 40, 40">
<Grid ColumnDefinitions="40, *, 40, 40, 40, 40">
<Button Grid.Column="0" x:Name="BackButton">
<i:MaterialIcon Kind="ArrowBack"> </i:MaterialIcon>
</Button>
<TextBlock Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" x:Name="ResourceStatus"></TextBlock>
<Button Grid.Column="2" x:Name="SettingsButton">
<Button Grid.Column="2" x:Name="LogButton">
<i:MaterialIcon Kind="ViewList"></i:MaterialIcon>
</Button>
<Button Grid.Column="3" x:Name="SettingsButton">
<i:MaterialIcon Kind="Gear"></i:MaterialIcon>
</Button>
<Button Grid.Column="3" x:Name="MinimizeButton">
<Button Grid.Column="4" x:Name="MinimizeButton">
<i:MaterialIcon Kind="WindowMinimize" />
</Button>
<Button Grid.Column="4" x:Name="CloseButton">
<Button Grid.Column="5" x:Name="CloseButton">
<i:MaterialIcon Kind="Close"/>
</Button>

View File

@ -31,6 +31,9 @@ namespace Wabbajack.App.Views
this.BindCommand(ViewModel, vm => vm.SettingsButton, view => view.SettingsButton)
.DisposeWith(dispose);
this.BindCommand(ViewModel, vm => vm.LogViewButton, view => view.LogButton)
.DisposeWith(dispose);
this.Bind(ViewModel, vm => vm.CurrentScreen, view => view.Contents.Content)
.DisposeWith(dispose);