mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
commit
0f73b20f46
@ -8,5 +8,7 @@ namespace Wabbajack.App
|
|||||||
public AbsolutePath SavedSettingsLocation { get; set; }
|
public AbsolutePath SavedSettingsLocation { get; set; }
|
||||||
|
|
||||||
public AbsolutePath EncryptedDataLocation { get; set; }
|
public AbsolutePath EncryptedDataLocation { get; set; }
|
||||||
|
|
||||||
|
public AbsolutePath LogLocation { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,9 +3,13 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:controls="clr-namespace:Wabbajack.App.Controls"
|
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"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Controls.LogView">
|
x:Class="Wabbajack.App.Controls.LogView">
|
||||||
<ScrollViewer ScrollChanged="ScrollViewer_OnScrollChanged" x:Name="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 x:Name="Messages">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
@ -19,4 +23,13 @@
|
|||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</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>
|
</UserControl>
|
@ -17,6 +17,8 @@ public partial class LogView : ReactiveUserControl<LogViewModel>
|
|||||||
{
|
{
|
||||||
this.OneWayBind(ViewModel, vm => vm.Messages, view => view.Messages.Items)
|
this.OneWayBind(ViewModel, vm => vm.Messages, view => view.Messages.Items)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
this.BindCommand(ViewModel, vm => vm.CopyLogFile, view => view.CopyLog)
|
||||||
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls.Mixins;
|
using Avalonia.Controls.Mixins;
|
||||||
|
using Avalonia.Input;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.Utilities;
|
using Wabbajack.App.Utilities;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
@ -12,26 +18,21 @@ namespace Wabbajack.App.Controls;
|
|||||||
public class LogViewModel : ViewModelBase, IActivatableViewModel
|
public class LogViewModel : ViewModelBase, IActivatableViewModel
|
||||||
{
|
{
|
||||||
private readonly LoggerProvider _provider;
|
private readonly LoggerProvider _provider;
|
||||||
|
public ReadOnlyObservableCollection<LoggerProvider.ILogMessage> Messages => _provider.MessageLog;
|
||||||
|
|
||||||
private readonly SourceCache<LoggerProvider.ILogMessage, long> _messages;
|
[Reactive]
|
||||||
|
public ReactiveCommand<Unit, Unit> CopyLogFile { get; set; }
|
||||||
public readonly ReadOnlyObservableCollection<LoggerProvider.ILogMessage> _messagesFiltered;
|
|
||||||
public ReadOnlyObservableCollection<LoggerProvider.ILogMessage> Messages => _messagesFiltered;
|
|
||||||
|
|
||||||
public LogViewModel(LoggerProvider provider)
|
public LogViewModel(LoggerProvider provider)
|
||||||
{
|
{
|
||||||
_messages = new SourceCache<LoggerProvider.ILogMessage, long>(m => m.MessageId);
|
|
||||||
//_messages.LimitSizeTo(100);
|
|
||||||
|
|
||||||
Activator = new ViewModelActivator();
|
Activator = new ViewModelActivator();
|
||||||
_provider = provider;
|
_provider = provider;
|
||||||
|
|
||||||
_messages.Connect()
|
CopyLogFile = ReactiveCommand.Create(() =>
|
||||||
.Bind(out _messagesFiltered)
|
{
|
||||||
.Subscribe();
|
var obj = new DataObject();
|
||||||
|
obj.Set(DataFormats.FileNames, new List<string> {_provider.LogPath.ToString()});
|
||||||
_provider.Messages
|
Application.Current.Clipboard.SetDataObjectAsync(obj);
|
||||||
.Subscribe(m => _messages.AddOrUpdate(m));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
11
Wabbajack.App/Screens/LogScreenView.axaml
Normal file
11
Wabbajack.App/Screens/LogScreenView.axaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<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:controls="clr-namespace:Wabbajack.App.Controls"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Wabbajack.App.Screens.LogScreenView">
|
||||||
|
<Grid RowDefinitions="*">
|
||||||
|
<controls:LogView></controls:LogView>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
12
Wabbajack.App/Screens/LogScreenView.axaml.cs
Normal file
12
Wabbajack.App/Screens/LogScreenView.axaml.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using ReactiveUI;
|
||||||
|
using Wabbajack.App.Views;
|
||||||
|
|
||||||
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
|
public partial class LogScreenView : ScreenBase<LogScreenViewModel>
|
||||||
|
{
|
||||||
|
public LogScreenView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
21
Wabbajack.App/Screens/LogScreenViewModel.cs
Normal file
21
Wabbajack.App/Screens/LogScreenViewModel.cs
Normal file
@ -0,0 +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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -45,6 +45,7 @@ namespace Wabbajack.App
|
|||||||
services.AddDTOSerializer();
|
services.AddDTOSerializer();
|
||||||
services.AddSingleton<ModeSelectionViewModel>();
|
services.AddSingleton<ModeSelectionViewModel>();
|
||||||
services.AddTransient<FileSelectionBoxViewModel>();
|
services.AddTransient<FileSelectionBoxViewModel>();
|
||||||
|
services.AddSingleton<IScreenView, LogScreenView>();
|
||||||
services.AddSingleton<IScreenView, ModeSelectionView>();
|
services.AddSingleton<IScreenView, ModeSelectionView>();
|
||||||
services.AddSingleton<IScreenView, InstallConfigurationView>();
|
services.AddSingleton<IScreenView, InstallConfigurationView>();
|
||||||
services.AddSingleton<IScreenView, CompilerConfigurationView>();
|
services.AddSingleton<IScreenView, CompilerConfigurationView>();
|
||||||
@ -57,6 +58,7 @@ namespace Wabbajack.App
|
|||||||
services.AddSingleton<InstallationStateManager>();
|
services.AddSingleton<InstallationStateManager>();
|
||||||
services.AddSingleton<HttpClient>();
|
services.AddSingleton<HttpClient>();
|
||||||
|
|
||||||
|
services.AddSingleton<LogScreenViewModel>();
|
||||||
services.AddAllSingleton<IReceiverMarker, StandardInstallationViewModel>();
|
services.AddAllSingleton<IReceiverMarker, StandardInstallationViewModel>();
|
||||||
services.AddAllSingleton<IReceiverMarker, InstallConfigurationViewModel>();
|
services.AddAllSingleton<IReceiverMarker, InstallConfigurationViewModel>();
|
||||||
services.AddAllSingleton<IReceiverMarker, CompilerConfigurationViewModel>();
|
services.AddAllSingleton<IReceiverMarker, CompilerConfigurationViewModel>();
|
||||||
@ -89,7 +91,8 @@ namespace Wabbajack.App
|
|||||||
{
|
{
|
||||||
EncryptedDataLocation = KnownFolders.WabbajackAppLocal.Combine("encrypted"),
|
EncryptedDataLocation = KnownFolders.WabbajackAppLocal.Combine("encrypted"),
|
||||||
ModListsDownloadLocation = KnownFolders.EntryPoint.Combine("downloaded_mod_lists"),
|
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>();
|
services.AddSingleton<SettingsManager>();
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using DynamicData;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Wabbajack.Paths;
|
||||||
|
using Wabbajack.Paths.IO;
|
||||||
|
|
||||||
namespace Wabbajack.App.Utilities;
|
namespace Wabbajack.App.Utilities;
|
||||||
|
|
||||||
@ -12,16 +19,67 @@ public class LoggerProvider : ILoggerProvider
|
|||||||
private Subject<ILogMessage> _messages = new();
|
private Subject<ILogMessage> _messages = new();
|
||||||
public IObservable<ILogMessage> Messages => _messages;
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_messages.Dispose();
|
_disposables.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ILogger CreateLogger(string categoryName)
|
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)
|
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)
|
public bool IsEnabled(LogLevel logLevel)
|
||||||
@ -63,10 +121,28 @@ public class LoggerProvider : ILoggerProvider
|
|||||||
long MessageId { get; }
|
long MessageId { get; }
|
||||||
|
|
||||||
string ShortMessage { 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 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -44,6 +44,9 @@ namespace Wabbajack.App.ViewModels
|
|||||||
[Reactive]
|
[Reactive]
|
||||||
public ReactiveCommand<Unit, Unit> SettingsButton { get; set; }
|
public ReactiveCommand<Unit, Unit> SettingsButton { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public ReactiveCommand<Unit, Unit> LogViewButton { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string ResourceStatus { get; set; }
|
public string ResourceStatus { get; set; }
|
||||||
|
|
||||||
@ -77,6 +80,12 @@ namespace Wabbajack.App.ViewModels
|
|||||||
})
|
})
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
LogViewButton = ReactiveCommand.Create(() =>
|
||||||
|
{
|
||||||
|
Receive(new NavigateTo(typeof(LogScreenViewModel)));
|
||||||
|
})
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
});
|
});
|
||||||
CurrentScreen = (Control)_screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel));
|
CurrentScreen = (Control)_screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel));
|
||||||
|
|
||||||
|
@ -34,21 +34,25 @@
|
|||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<Grid RowDefinitions="40, *">
|
<Grid RowDefinitions="40, *">
|
||||||
<Grid ColumnDefinitions="40, *, 40, 40, 40">
|
<Grid ColumnDefinitions="40, *, 40, 40, 40, 40">
|
||||||
<Button Grid.Column="0" x:Name="BackButton">
|
<Button Grid.Column="0" x:Name="BackButton">
|
||||||
<i:MaterialIcon Kind="ArrowBack"> </i:MaterialIcon>
|
<i:MaterialIcon Kind="ArrowBack"> </i:MaterialIcon>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<TextBlock Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" x:Name="ResourceStatus"></TextBlock>
|
<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>
|
<i:MaterialIcon Kind="Gear"></i:MaterialIcon>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Grid.Column="3" x:Name="MinimizeButton">
|
<Button Grid.Column="4" x:Name="MinimizeButton">
|
||||||
<i:MaterialIcon Kind="WindowMinimize" />
|
<i:MaterialIcon Kind="WindowMinimize" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button Grid.Column="4" x:Name="CloseButton">
|
<Button Grid.Column="5" x:Name="CloseButton">
|
||||||
<i:MaterialIcon Kind="Close"/>
|
<i:MaterialIcon Kind="Close"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
@ -31,6 +31,9 @@ namespace Wabbajack.App.Views
|
|||||||
this.BindCommand(ViewModel, vm => vm.SettingsButton, view => view.SettingsButton)
|
this.BindCommand(ViewModel, vm => vm.SettingsButton, view => view.SettingsButton)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(dispose);
|
||||||
|
|
||||||
|
this.BindCommand(ViewModel, vm => vm.LogViewButton, view => view.LogButton)
|
||||||
|
.DisposeWith(dispose);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.CurrentScreen, view => view.Contents.Content)
|
this.Bind(ViewModel, vm => vm.CurrentScreen, view => view.Contents.Content)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(dispose);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user