GUI: Implement plugin interface
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
// Copyright (c) 2020 Katy Coe - https://www.djkaty.com - https://github.com/djkaty
|
/*
|
||||||
// All rights reserved
|
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -22,7 +25,7 @@ namespace Il2CppInspectorGUI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : Application, INotifyPropertyChanged
|
public partial class App : Application, INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
// Catch unhandled exceptions for debugging startup failures
|
// Catch unhandled exceptions for debugging startup failures and plugins
|
||||||
public App() : base() {
|
public App() : base() {
|
||||||
var np = Environment.NewLine + Environment.NewLine;
|
var np = Environment.NewLine + Environment.NewLine;
|
||||||
|
|
||||||
@@ -30,7 +33,11 @@ namespace Il2CppInspectorGUI
|
|||||||
MessageBox.Show(e.Exception.GetType() + ": " + e.Exception.Message
|
MessageBox.Show(e.Exception.GetType() + ": " + e.Exception.Message
|
||||||
+ np + e.Exception.StackTrace
|
+ np + e.Exception.StackTrace
|
||||||
+ np + "More details may follow in subsequent error boxes",
|
+ np + "More details may follow in subsequent error boxes",
|
||||||
"Oopsie... Il2CppInspector could not start up");
|
"Il2CppInspector encountered a fatal error");
|
||||||
|
|
||||||
|
if (e.Exception is FileNotFoundException fe)
|
||||||
|
MessageBox.Show("Missing file: " + fe.FileName, "Additional error information");
|
||||||
|
|
||||||
var ex = e.Exception;
|
var ex = e.Exception;
|
||||||
while (ex.InnerException != null) {
|
while (ex.InnerException != null) {
|
||||||
MessageBox.Show(ex.GetType() + ": " + ex.Message + np
|
MessageBox.Show(ex.GetType() + ": " + ex.Message + np
|
||||||
@@ -68,7 +75,16 @@ namespace Il2CppInspectorGUI
|
|||||||
|
|
||||||
// Initialization entry point
|
// Initialization entry point
|
||||||
protected override void OnStartup(StartupEventArgs e) {
|
protected override void OnStartup(StartupEventArgs e) {
|
||||||
|
// Set contents of load options window
|
||||||
ResetLoadOptions();
|
ResetLoadOptions();
|
||||||
|
|
||||||
|
// Set handlers for plugin manager
|
||||||
|
PluginManager.ErrorHandler += (s, e) => {
|
||||||
|
MessageBox.Show($"The plugin {e.Plugin.Name} encountered an error while executing {e.Operation}: {e.Exception.Message}."
|
||||||
|
+ Environment.NewLine + Environment.NewLine + "The application will continue but may not behave as expected.", "Plugin error");
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginManager.StatusHandler += (s, e) => StatusUpdate(e.Plugin, "Plugin " + e.Plugin.Name + ": " + e.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetLoadOptions() {
|
public void ResetLoadOptions() {
|
||||||
|
|||||||
30
Il2CppInspector.GUI/EqualityConverter.cs
Normal file
30
Il2CppInspector.GUI/EqualityConverter.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2020 Katy Coe - https://www.djkaty.com - https://github.com/djkaty
|
||||||
|
// All rights reserved
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace Il2CppInspector.GUI
|
||||||
|
{
|
||||||
|
// Adapted from https://stackoverflow.com/a/37307169 and https://stackoverflow.com/a/28316967
|
||||||
|
internal class EqualityConverter : IMultiValueConverter
|
||||||
|
{
|
||||||
|
public object TrueValue { get; set; }
|
||||||
|
public object FalseValue { get; set; }
|
||||||
|
|
||||||
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
|
||||||
|
if (values.Length < 2)
|
||||||
|
return FalseValue;
|
||||||
|
|
||||||
|
return values[0].Equals(values[1]) ? TrueValue : FalseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,16 @@ namespace Il2CppInspector.GUI
|
|||||||
if (value == null || targetType != typeof(string))
|
if (value == null || targetType != typeof(string))
|
||||||
return DependencyProperty.UnsetValue;
|
return DependencyProperty.UnsetValue;
|
||||||
|
|
||||||
return ((ulong) value).ToString("x16");
|
return value switch {
|
||||||
|
ulong n => n.ToString("x16"),
|
||||||
|
long n => n.ToString("x16"),
|
||||||
|
uint n => n.ToString("x8"),
|
||||||
|
int n => n.ToString("x8"),
|
||||||
|
ushort n => n.ToString("x4"),
|
||||||
|
short n => n.ToString("x4"),
|
||||||
|
byte n => n.ToString("x2"),
|
||||||
|
_ => throw new NotImplementedException("Unknown number format")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
<PublishTrimmed>true</PublishTrimmed>
|
<!-- Plugins may require bass class library assemblies we're not using so disable trimming -->
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<AssemblyName>Il2CppInspector</AssemblyName>
|
<AssemblyName>Il2CppInspector</AssemblyName>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
// Copyright (c) 2020 Katy Coe - https://www.djkaty.com - https://github.com/djkaty
|
/*
|
||||||
// All rights reserved
|
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|||||||
@@ -434,8 +434,13 @@
|
|||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- Load options -->
|
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<Button Grid.Row="2" Name="btnLoadOptions" Style="{StaticResource LightBoxButton}" Click="BtnLoadOptions_Click" Margin="0,0,10,10" Padding="5" HorizontalAlignment="Right" VerticalAlignment="Bottom" FontSize="18" Width="180" Content="Import options..."/>
|
<!-- Plugin options -->
|
||||||
|
<Button Name="btnPluginOptions" Style="{StaticResource LightBoxButton}" Click="BtnPluginOptions_Click" Margin="0,0,10,10" Padding="5" HorizontalAlignment="Right" VerticalAlignment="Bottom" FontSize="18" Width="180" Content="Manage plugins..."/>
|
||||||
|
|
||||||
|
<!-- Load options -->
|
||||||
|
<Button Name="btnLoadOptions" Style="{StaticResource LightBoxButton}" Click="BtnLoadOptions_Click" Margin="0,0,10,10" Padding="5" HorizontalAlignment="Right" VerticalAlignment="Bottom" FontSize="18" Width="180" Content="Import options..."/>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Button Name="btnSelectBinaryFile" Style="{StaticResource LightBoxButton}" Margin="100" Click="BtnSelectBinaryFile_OnClick" Visibility="Hidden">
|
<Button Name="btnSelectBinaryFile" Style="{StaticResource LightBoxButton}" Margin="100" Click="BtnSelectBinaryFile_OnClick" Visibility="Hidden">
|
||||||
<TextBlock TextAlignment="Center">
|
<TextBlock TextAlignment="Center">
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
// Copyright (c) 2020 Katy Coe - https://www.djkaty.com - https://github.com/djkaty
|
/*
|
||||||
// All rights reserved
|
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -71,6 +74,15 @@ namespace Il2CppInspectorGUI
|
|||||||
Process.Start(new ProcessStartInfo {FileName = e.Uri.ToString(), UseShellExecute = true});
|
Process.Start(new ProcessStartInfo {FileName = e.Uri.ToString(), UseShellExecute = true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Open Manage Plugins dialog
|
||||||
|
/// </summary>
|
||||||
|
private void BtnPluginOptions_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var configDlg = new PluginManagerDialog();
|
||||||
|
configDlg.Owner = this;
|
||||||
|
configDlg.ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Open Load Options dialog
|
/// Open Load Options dialog
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -109,7 +121,7 @@ namespace Il2CppInspectorGUI
|
|||||||
else {
|
else {
|
||||||
areaBusyIndicator.Visibility = Visibility.Hidden;
|
areaBusyIndicator.Visibility = Visibility.Hidden;
|
||||||
grdFirstPage.Visibility = Visibility.Visible;
|
grdFirstPage.Visibility = Visibility.Visible;
|
||||||
MessageBox.Show(this, app.LastException.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
MessageBox.Show(this, app.LastException.Message + Environment.NewLine + app.LastException.StackTrace, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +157,7 @@ namespace Il2CppInspectorGUI
|
|||||||
else {
|
else {
|
||||||
areaBusyIndicator.Visibility = Visibility.Hidden;
|
areaBusyIndicator.Visibility = Visibility.Hidden;
|
||||||
btnSelectBinaryFile.Visibility = Visibility.Visible;
|
btnSelectBinaryFile.Visibility = Visibility.Visible;
|
||||||
MessageBox.Show(this, app.LastException.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
MessageBox.Show(this, app.LastException.Message + Environment.NewLine + app.LastException.StackTrace, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +195,7 @@ namespace Il2CppInspectorGUI
|
|||||||
else {
|
else {
|
||||||
areaBusyIndicator.Visibility = Visibility.Hidden;
|
areaBusyIndicator.Visibility = Visibility.Hidden;
|
||||||
grdFirstPage.Visibility = Visibility.Visible;
|
grdFirstPage.Visibility = Visibility.Visible;
|
||||||
MessageBox.Show(this, app.LastException.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
MessageBox.Show(this, app.LastException.Message + Environment.NewLine + app.LastException.StackTrace, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
186
Il2CppInspector.GUI/PluginConfigurationDialog.xaml
Normal file
186
Il2CppInspector.GUI/PluginConfigurationDialog.xaml
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<Window x:Class="Il2CppInspector.GUI.PluginConfigurationDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
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:local="clr-namespace:Il2CppInspector.GUI"
|
||||||
|
xmlns:pluginapi="clr-namespace:Il2CppInspector.PluginAPI.V100;assembly=Il2CppInspector.Common"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="{Binding Path=Plugin.Name, StringFormat=Configuration for {0}}" Height="400" Width="700"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
SizeToContent="Height" MaxHeight="800">
|
||||||
|
<Window.Resources>
|
||||||
|
<local:OptionTemplateSelector x:Key="OptionTemplateSelector"/>
|
||||||
|
<local:HexStringValueConverter x:Key="HexStringValueConverter" />
|
||||||
|
<local:EqualityConverter x:Key="EqualityVisibilityConverter" TrueValue="{x:Static Visibility.Visible}" FalseValue="{x:Static Visibility.Collapsed}" />
|
||||||
|
<BooleanToVisibilityConverter x:Key="VisibleIfTrueConverter" />
|
||||||
|
|
||||||
|
<!-- Option layouts -->
|
||||||
|
<DataTemplate x:Key="TextTemplate">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Width="250" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Path=Description}"></TextBlock>
|
||||||
|
<TextBlock Visibility="{Binding Required, Converter={StaticResource VisibleIfTrueConverter}}" Text="*" Foreground="Red"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBox DockPanel.Dock="Right" VerticalAlignment="Center" Padding="2" Margin="0,4,4,4" Text="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged}"></TextBox>
|
||||||
|
</DockPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="FilePathTemplate">
|
||||||
|
<DockPanel>
|
||||||
|
<Button Name="btnFilePathSelector" DockPanel.Dock="Right" Width="70" Margin="4" Click="btnFilePathSelector_Click">Browse</Button>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Width="250" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Description}"></TextBlock>
|
||||||
|
<TextBlock Visibility="{Binding Required, Converter={StaticResource VisibleIfTrueConverter}}" Text="*" Foreground="Red"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Name="txtFilePathSelector" DockPanel.Dock="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="3" ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Text}" Text="{Binding Value}"/>
|
||||||
|
</DockPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="NumberTemplate">
|
||||||
|
<StackPanel>
|
||||||
|
<!-- Decimal number -->
|
||||||
|
<DockPanel>
|
||||||
|
<DockPanel.Visibility>
|
||||||
|
<MultiBinding Converter="{StaticResource EqualityVisibilityConverter}">
|
||||||
|
<Binding Path="Style" />
|
||||||
|
<Binding Source="{x:Static pluginapi:PluginOptionNumberStyle.Decimal}" Mode="OneWay" />
|
||||||
|
</MultiBinding>
|
||||||
|
</DockPanel.Visibility>
|
||||||
|
|
||||||
|
<TextBlock DockPanel.Dock="Left" Width="250" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Description}"></TextBlock>
|
||||||
|
<TextBlock Visibility="{Binding Required, Converter={StaticResource VisibleIfTrueConverter}}" Text="*" Foreground="Red"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBox Name="txtDecimalString" DockPanel.Dock="Right" VerticalAlignment="Center" Padding="2" Margin="0,4,4,4" Text="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged}"></TextBox>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Hex number -->
|
||||||
|
<DockPanel>
|
||||||
|
<DockPanel.Visibility>
|
||||||
|
<MultiBinding Converter="{StaticResource EqualityVisibilityConverter}">
|
||||||
|
<Binding Path="Style" />
|
||||||
|
<Binding Source="{x:Static pluginapi:PluginOptionNumberStyle.Hex}" Mode="OneWay" />
|
||||||
|
</MultiBinding>
|
||||||
|
</DockPanel.Visibility>
|
||||||
|
|
||||||
|
<TextBlock DockPanel.Dock="Left" Width="250" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Description}"></TextBlock>
|
||||||
|
<TextBlock Visibility="{Binding Required, Converter={StaticResource VisibleIfTrueConverter}}" Text="*" Foreground="Red"/>
|
||||||
|
</TextBlock>
|
||||||
|
<DockPanel HorizontalAlignment="Stretch">
|
||||||
|
<Label Margin="0,3,3,5">0x</Label>
|
||||||
|
<TextBox Name="txtHexString" Padding="2" Margin="0,6,4,6" Text="{Binding Value, Converter={StaticResource HexStringValueConverter}, UpdateSourceTrigger=PropertyChanged}" PreviewTextInput="txtHexString_PreviewTextInput"/>
|
||||||
|
</DockPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="BooleanTemplate">
|
||||||
|
<DockPanel Margin="250,0,0,0">
|
||||||
|
<TextBlock DockPanel.Dock="Left" VerticalAlignment="Top" Margin="0,10,2,6"
|
||||||
|
Visibility="{Binding Required, Converter={StaticResource VisibleIfTrueConverter}}" Text="*" Foreground="Red"/>
|
||||||
|
<CheckBox DockPanel.Dock="Right" VerticalAlignment="Center" Margin="0,10,2,6" IsChecked="{Binding Value}">
|
||||||
|
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Description}" />
|
||||||
|
</CheckBox>
|
||||||
|
</DockPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="ChoiceTemplate">
|
||||||
|
<StackPanel>
|
||||||
|
<!-- Drop-down box version -->
|
||||||
|
<DockPanel>
|
||||||
|
<DockPanel.Visibility>
|
||||||
|
<MultiBinding Converter="{StaticResource EqualityVisibilityConverter}">
|
||||||
|
<Binding Path="Style" />
|
||||||
|
<Binding Source="{x:Static pluginapi:PluginOptionChoiceStyle.Dropdown}" Mode="OneWay" />
|
||||||
|
</MultiBinding>
|
||||||
|
</DockPanel.Visibility>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Width="250" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Description}"></TextBlock>
|
||||||
|
<TextBlock Visibility="{Binding Required, Converter={StaticResource VisibleIfTrueConverter}}" Text="*" Foreground="Red"/>
|
||||||
|
</TextBlock>
|
||||||
|
<ComboBox DockPanel.Dock="Right" Margin="4,8,4,8" ItemsSource="{Binding Choices}" DisplayMemberPath="Value" SelectedValuePath="Key" SelectedValue="{Binding Value}"></ComboBox>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Radio buttons version -->
|
||||||
|
<DockPanel>
|
||||||
|
<DockPanel.Visibility>
|
||||||
|
<MultiBinding Converter="{StaticResource EqualityVisibilityConverter}">
|
||||||
|
<Binding Path="Style" />
|
||||||
|
<Binding Source="{x:Static pluginapi:PluginOptionChoiceStyle.List}" Mode="OneWay" />
|
||||||
|
</MultiBinding>
|
||||||
|
</DockPanel.Visibility>
|
||||||
|
<GroupBox Header="{Binding Description}" Margin="5" Padding="5">
|
||||||
|
<ListBox ItemsSource="{Binding Choices}" SelectedItem="{Binding Value}" BorderBrush="Transparent">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<RadioButton GroupName="{Binding Header, RelativeSource={RelativeSource AncestorType=GroupBox}}"
|
||||||
|
Content="{Binding}"
|
||||||
|
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
|
||||||
|
Focusable="False"
|
||||||
|
IsHitTestVisible="False"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</GroupBox>
|
||||||
|
</DockPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Configure ListBox to display configuration controls nicely -->
|
||||||
|
<Style TargetType="{x:Type ListBoxItem}">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
|
||||||
|
<!-- Force ListBox to width of window -->
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
|
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
|
||||||
|
<Setter Property="Padding" Value="2,0,0,0"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="{x:Type ListBoxItem}">
|
||||||
|
<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="false">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid Margin="10">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="50" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="50" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<Label Grid.Row="0" Foreground="{StaticResource WindowsBlue}" FontSize="20" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||||
|
Content="{Binding Plugin.Name}" ContentStringFormat="Configuration for {0}" Padding="8,0,0,0"/>
|
||||||
|
|
||||||
|
<!-- Options editor -->
|
||||||
|
<ListBox Grid.Row="1" Name="lstOptions" ItemsSource="{Binding Plugin.Options}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" Padding="8" BorderBrush="Transparent">
|
||||||
|
<ListBox.ItemTemplateSelector>
|
||||||
|
<local:OptionTemplateSelector TextTemplate="{StaticResource TextTemplate}"
|
||||||
|
FilePathTemplate="{StaticResource FilePathTemplate}"
|
||||||
|
NumberTemplate="{StaticResource NumberTemplate}"
|
||||||
|
BooleanTemplate="{StaticResource BooleanTemplate}"
|
||||||
|
ChoiceTemplate="{StaticResource ChoiceTemplate}">
|
||||||
|
</local:OptionTemplateSelector>
|
||||||
|
</ListBox.ItemTemplateSelector>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<!-- Accept button -->
|
||||||
|
<StackPanel Grid.Row="2" VerticalAlignment="Bottom" Margin="10">
|
||||||
|
<Button Name="okButton" Click="okButton_Click" Style="{StaticResource LightBoxButton}" IsDefault="True" Width="150" FontSize="18">OK</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
93
Il2CppInspector.GUI/PluginConfigurationDialog.xaml.cs
Normal file
93
Il2CppInspector.GUI/PluginConfigurationDialog.xaml.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using Il2CppInspectorGUI;
|
||||||
|
using System.Windows.Forms.Design;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Linq;
|
||||||
|
using Il2CppInspector.PluginAPI.V100;
|
||||||
|
using static Il2CppInspector.PluginManager;
|
||||||
|
|
||||||
|
namespace Il2CppInspector.GUI
|
||||||
|
{
|
||||||
|
// Class which selects the correct control to display for each plugin option
|
||||||
|
public class OptionTemplateSelector : DataTemplateSelector
|
||||||
|
{
|
||||||
|
public DataTemplate TextTemplate { get; set; }
|
||||||
|
public DataTemplate FilePathTemplate { get; set; }
|
||||||
|
public DataTemplate NumberTemplate { get; set; }
|
||||||
|
public DataTemplate BooleanTemplate { get; set; }
|
||||||
|
public DataTemplate ChoiceTemplate { get; set; }
|
||||||
|
|
||||||
|
// Use some fancy reflection to get the right template property
|
||||||
|
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
|
||||||
|
var option = (IPluginOption) item;
|
||||||
|
return (DataTemplate) GetType().GetProperty(option.GetType().Name.Split("`")[0]["PluginOption".Length..] + "Template").GetValue(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for PluginConfigurationDialog.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class PluginConfigurationDialog : Window
|
||||||
|
{
|
||||||
|
// Item to configure
|
||||||
|
public IPlugin Plugin { get; }
|
||||||
|
|
||||||
|
public PluginConfigurationDialog(IPlugin plugin) {
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = this;
|
||||||
|
Plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void okButton_Click(object sender, RoutedEventArgs e) {
|
||||||
|
// Closes dialog box automatically
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select a file path
|
||||||
|
private void btnFilePathSelector_Click(object sender, RoutedEventArgs e) {
|
||||||
|
|
||||||
|
var option = (PluginOptionFilePath) ((Button) sender).DataContext;
|
||||||
|
|
||||||
|
var openFileDialog = new OpenFileDialog {
|
||||||
|
Title = option.Description,
|
||||||
|
Filter = "All files (*.*)|*.*",
|
||||||
|
FileName = option.Value,
|
||||||
|
CheckFileExists = true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (openFileDialog.ShowDialog() == true) {
|
||||||
|
option.Value = openFileDialog.FileName;
|
||||||
|
|
||||||
|
// This spaghetti saves us from implementing INotifyPropertyChanged on Plugin.Options
|
||||||
|
// (we don't want to expose WPF stuff in our SDK)
|
||||||
|
// Will break if we change the format of the FilePathDataTemplate too much
|
||||||
|
var tb = ((DockPanel) ((Button) sender).Parent).Children.OfType<TextBlock>().First(n => n.Name == "txtFilePathSelector");
|
||||||
|
tb.Text = option.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow hex characters in hex string
|
||||||
|
private void txtHexString_PreviewTextInput(object sender, TextCompositionEventArgs e) {
|
||||||
|
e.Handled = !Regex.IsMatch(e.Text, @"[A-Fa-f0-9]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
139
Il2CppInspector.GUI/PluginManagerDialog.xaml
Normal file
139
Il2CppInspector.GUI/PluginManagerDialog.xaml
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<Window x:Class="Il2CppInspector.GUI.PluginManagerDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
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:local="clr-namespace:Il2CppInspector.GUI"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Plugin Manager" Height="450" Width="800"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
<Window.Resources>
|
||||||
|
<!-- Configure ListBox to display plugins nicely -->
|
||||||
|
<Style x:Key="ConfigItemStyle" TargetType="{x:Type ListBoxItem}">
|
||||||
|
|
||||||
|
<!-- Alternating background colours -->
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
|
||||||
|
<Setter Property="Background" Value="AliceBlue" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
|
||||||
|
<Setter Property="Background" Value="AliceBlue" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="false">
|
||||||
|
<Setter Property="Background" Value="LightGray"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
|
||||||
|
<!-- Force ListBox to width of window -->
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
|
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
|
||||||
|
<Setter Property="Padding" Value="2,0,0,0"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="{x:Type ListBoxItem}">
|
||||||
|
<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<!-- Remove highlight on hover and selection; set border on selection instead -->
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource MicrosoftBlue}" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid Margin="10">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="50" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="70" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="60" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<Label Grid.Row="0" Foreground="{StaticResource WindowsBlue}" FontSize="20" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||||
|
Content="Plugin Manager" Padding="8,0,0,0"/>
|
||||||
|
|
||||||
|
<!-- Plugins editor -->
|
||||||
|
<ListBox Grid.Row="1" Name="lstPlugins" ItemsSource="{Binding ManagedPlugins}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" BorderBrush="Transparent" AlternationCount="2" SelectionChanged="lstPlugins_SelectionChanged">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="130" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" MinHeight="60" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<CheckBox Grid.Column="0" Margin="6" VerticalAlignment="Center" IsChecked="{Binding Enabled}">
|
||||||
|
<CheckBox.LayoutTransform>
|
||||||
|
<ScaleTransform ScaleX="1.5" ScaleY="1.5" />
|
||||||
|
</CheckBox.LayoutTransform>
|
||||||
|
</CheckBox>
|
||||||
|
<TextBlock Grid.Column="1" Margin="0,10,0,10" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Plugin.Name}" FontSize="14" Foreground="{StaticResource WindowsBlue}" />
|
||||||
|
<TextBlock Text="{Binding Plugin.Author, StringFormat={}by {0}}" />
|
||||||
|
<TextBlock Text="{Binding Plugin.Version, StringFormat={}[{0}]}" />
|
||||||
|
<LineBreak/>
|
||||||
|
<TextBlock Text="{Binding Plugin.Description}" TextWrapping="Wrap" />
|
||||||
|
</TextBlock>
|
||||||
|
<Button Grid.Column="2" Margin="15" Padding="5" VerticalAlignment="Center" Content="Configure..." Click="btnConfig_Click">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="{x:Type Button}">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding Plugin.Options}" Value="{x:Null}">
|
||||||
|
<Setter Property="Visibility" Value="Hidden"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
|
<ListBox.ItemContainerStyle>
|
||||||
|
<Style BasedOn="{StaticResource ConfigItemStyle}" TargetType="{x:Type ListBoxItem}">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding Path=Available}" Value="False">
|
||||||
|
<Setter Property="ListBoxItem.IsEnabled" Value="False"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ListBox.ItemContainerStyle>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<!-- Reordering controls -->
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="1" Margin="10,0,0,0">
|
||||||
|
<Button Name="btnTop" FontFamily="Segoe MDL2 Assets" Style="{StaticResource LightBoxButton}" FontSize="14" Padding="5,8,5,8" Margin="2" Content="" Click="btnTop_Click"/>
|
||||||
|
<Button Name="btnUp" FontFamily="Segoe MDL2 Assets" Style="{StaticResource LightBoxButton}" FontSize="20" Padding="5" Margin="2" Content="" Click="btnUp_Click"/>
|
||||||
|
<Button Name="btnDown" FontFamily="Segoe MDL2 Assets" Style="{StaticResource LightBoxButton}" FontSize="20" Padding="5" Margin="2" Content="" Click="btnDown_Click"/>
|
||||||
|
<Button Name="btnBottom" FontFamily="Segoe MDL2 Assets" Style="{StaticResource LightBoxButton}" FontSize="14" Padding="5,8,5,8" Margin="2" Content="" Click="btnBottom_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Bottom buttons -->
|
||||||
|
<DockPanel Grid.Row="2" Grid.ColumnSpan="2" VerticalAlignment="Bottom" Margin="10">
|
||||||
|
<!-- Accept button -->
|
||||||
|
<Button DockPanel.Dock="Right" Name="okButton" Click="okButton_Click" Style="{StaticResource LightBoxButton}" IsDefault="True" Width="150" FontSize="18" Padding="5">OK</Button>
|
||||||
|
|
||||||
|
<!-- Refresh button -->
|
||||||
|
<Button DockPanel.Dock="Left" Name="refreshButton" Click="refreshButton_Click" Style="{StaticResource LightBoxButton}" HorizontalAlignment="Left" Width="150" FontSize="18" Padding="5">Refresh</Button>
|
||||||
|
|
||||||
|
<!-- Get Plugins button -->
|
||||||
|
<Button DockPanel.Dock="Left" Name="getPluginsButton" Click="getPluginsButton_Click" Style="{StaticResource LightBoxButton}" HorizontalAlignment="Left" Margin="10,0,0,0" Width="150" FontSize="18" Padding="5">Get Plugins...</Button>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
112
Il2CppInspector.GUI/PluginManagerDialog.xaml.cs
Normal file
112
Il2CppInspector.GUI/PluginManagerDialog.xaml.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
|
||||||
|
namespace Il2CppInspector.GUI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for PluginManagerDialog.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class PluginManagerDialog : Window
|
||||||
|
{
|
||||||
|
public PluginManagerDialog() {
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = PluginManager.AsInstance;
|
||||||
|
|
||||||
|
// Set default re-order button state
|
||||||
|
lstPlugins_SelectionChanged(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void okButton_Click(object sender, RoutedEventArgs e) {
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload list of plugins and reset settings
|
||||||
|
private void refreshButton_Click(object sender, RoutedEventArgs e) {
|
||||||
|
PluginManager.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open configuration for specific plugin
|
||||||
|
private void btnConfig_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var plugin = (ManagedPlugin) ((Button) sender).DataContext;
|
||||||
|
|
||||||
|
var configDlg = new PluginConfigurationDialog(plugin.Plugin);
|
||||||
|
configDlg.Owner = this;
|
||||||
|
configDlg.ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-ordering controls
|
||||||
|
private void lstPlugins_SelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||||
|
var index = lstPlugins.SelectedIndex;
|
||||||
|
|
||||||
|
btnTop.IsEnabled = btnUp.IsEnabled = index > 0;
|
||||||
|
btnBottom.IsEnabled = btnDown.IsEnabled = index > -1 && index < lstPlugins.Items.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void btnUp_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var plugins = PluginManager.AsInstance.ManagedPlugins;
|
||||||
|
|
||||||
|
var index = lstPlugins.SelectedIndex;
|
||||||
|
var item = (ManagedPlugin) lstPlugins.SelectedItem;
|
||||||
|
|
||||||
|
plugins.Remove(item);
|
||||||
|
plugins.Insert(index - 1, item);
|
||||||
|
lstPlugins.SelectedIndex = index - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void btnDown_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var plugins = PluginManager.AsInstance.ManagedPlugins;
|
||||||
|
|
||||||
|
var index = lstPlugins.SelectedIndex;
|
||||||
|
var item = (ManagedPlugin) lstPlugins.SelectedItem;
|
||||||
|
|
||||||
|
plugins.Remove(item);
|
||||||
|
plugins.Insert(index + 1, item);
|
||||||
|
lstPlugins.SelectedIndex = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void btnTop_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var plugins = PluginManager.AsInstance.ManagedPlugins;
|
||||||
|
|
||||||
|
var index = lstPlugins.SelectedIndex;
|
||||||
|
var item = (ManagedPlugin) lstPlugins.SelectedItem;
|
||||||
|
|
||||||
|
plugins.Remove(item);
|
||||||
|
plugins.Insert(0, item);
|
||||||
|
lstPlugins.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void btnBottom_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var plugins = PluginManager.AsInstance.ManagedPlugins;
|
||||||
|
|
||||||
|
var index = lstPlugins.SelectedIndex;
|
||||||
|
var item = (ManagedPlugin) lstPlugins.SelectedItem;
|
||||||
|
|
||||||
|
plugins.Remove(item);
|
||||||
|
plugins.Add(item);
|
||||||
|
lstPlugins.SelectedIndex = plugins.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get plugins button
|
||||||
|
private void getPluginsButton_Click(object sender, RoutedEventArgs e) {
|
||||||
|
Process.Start(new ProcessStartInfo { FileName = @"https://github.com/djkaty/Il2CppInspectorPlugins", UseShellExecute = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user