Added a lot to the doc and modified the pdf generation
This commit is contained in:
+410
-51
@@ -1,8 +1,8 @@
|
||||
/// Author : Maxime Rohmer
|
||||
/// Date : 08/05/2023
|
||||
/// Date : 30/05/2023
|
||||
/// File : Window.cs
|
||||
/// Brief : Default Window object that is mainly expected to be inherited.
|
||||
/// Version : 0.1
|
||||
/// Version : Alpha 1.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -15,10 +15,19 @@ using Tesseract;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Drawing.Drawing2D;
|
||||
|
||||
namespace Test_Merge
|
||||
namespace TrackTrends
|
||||
{
|
||||
public class Window
|
||||
{
|
||||
public const string STRING_DEBUG_FOLDER = "./GetString";
|
||||
public const string LAPTIME_DEBUG_FOLDER = "./LapTime";
|
||||
public const string GAPTOLEADER_DEBUG_FOLDER = "./Gap";
|
||||
public const string SECTOR1_DEBUG_FOLDER = "./Sector1";
|
||||
public const string SECTOR2_DEBUG_FOLDER = "./Sector2";
|
||||
public const string SECTOR3_DEBUG_FOLDER = "./Sector3";
|
||||
public const string DRS_DEBUG_FOLDER = "./DRS";
|
||||
public const string TYRE_DEBUG_FOLDER = "./Tyre";
|
||||
|
||||
private Rectangle _bounds;
|
||||
private Bitmap _image;
|
||||
private string _name;
|
||||
@@ -28,6 +37,8 @@ namespace Test_Merge
|
||||
public string Name { get => _name; protected set => _name = value; }
|
||||
//This will have to be changed if you want to make it run on your machine
|
||||
public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@"C:\Users\Moi\Pictures\SeleniumScreens\TessData");
|
||||
//Debug
|
||||
public static Random rnd = new Random();
|
||||
|
||||
public Bitmap WindowImage
|
||||
{
|
||||
@@ -40,6 +51,12 @@ namespace Test_Merge
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates a new Window
|
||||
/// </summary>
|
||||
/// <param name="image">The image of the parent zone</param>
|
||||
/// <param name="bounds">The position and size of the window</param>
|
||||
/// <param name="generateEngine">Does the window need to generate a tesseract engine (takes time and ressources)</param>
|
||||
public Window(Bitmap image, Rectangle bounds, bool generateEngine = true)
|
||||
{
|
||||
Image = image;
|
||||
@@ -49,12 +66,32 @@ namespace Test_Merge
|
||||
Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
|
||||
Engine.DefaultPageSegMode = PageSegMode.SingleLine;
|
||||
}
|
||||
|
||||
//DEBUG
|
||||
/*
|
||||
if (!Directory.Exists(STRING_DEBUG_FOLDER))
|
||||
Directory.CreateDirectory(STRING_DEBUG_FOLDER);
|
||||
if (!Directory.Exists(LAPTIME_DEBUG_FOLDER))
|
||||
Directory.CreateDirectory(LAPTIME_DEBUG_FOLDER);
|
||||
if (!Directory.Exists(GAPTOLEADER_DEBUG_FOLDER))
|
||||
Directory.CreateDirectory(GAPTOLEADER_DEBUG_FOLDER);
|
||||
if (!Directory.Exists(SECTOR1_DEBUG_FOLDER))
|
||||
Directory.CreateDirectory(SECTOR1_DEBUG_FOLDER);
|
||||
if (!Directory.Exists(SECTOR2_DEBUG_FOLDER))
|
||||
Directory.CreateDirectory(SECTOR2_DEBUG_FOLDER);
|
||||
if (!Directory.Exists(SECTOR3_DEBUG_FOLDER))
|
||||
Directory.CreateDirectory(SECTOR3_DEBUG_FOLDER);
|
||||
if (!Directory.Exists(DRS_DEBUG_FOLDER))
|
||||
Directory.CreateDirectory(DRS_DEBUG_FOLDER);
|
||||
if (!Directory.Exists(TYRE_DEBUG_FOLDER))
|
||||
Directory.CreateDirectory(TYRE_DEBUG_FOLDER);
|
||||
*/
|
||||
}
|
||||
/// <summary>
|
||||
/// Method that will have to be used by the childrens to let the model make them decode the images they have
|
||||
/// </summary>
|
||||
/// <returns>Returns an object because we dont know what kind of return it will be</returns>
|
||||
public virtual async Task<Object> DecodePng()
|
||||
public virtual Object DecodePng()
|
||||
{
|
||||
return "NaN";
|
||||
}
|
||||
@@ -63,7 +100,7 @@ namespace Test_Merge
|
||||
/// </summary>
|
||||
/// <param name="driverList">This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short</param>
|
||||
/// <returns>Returns an object because we dont know what kind of return it will be</returns>
|
||||
public virtual async Task<Object> DecodePng(List<string> driverList)
|
||||
public virtual Object DecodePng(List<string> driverList)
|
||||
{
|
||||
return "NaN";
|
||||
}
|
||||
@@ -87,12 +124,15 @@ namespace Test_Merge
|
||||
/// <param name="windowType">The type of window it is</param>
|
||||
/// <param name="Engine">The Tesseract Engine</param>
|
||||
/// <returns>The time in milliseconds</returns>
|
||||
public static async Task<int> GetTimeFromPng(Bitmap windowImage, OcrImage.WindowType windowType, TesseractEngine Engine)
|
||||
public static int GetTimeFromPng(Bitmap image, OcrImage.WindowType windowType, TesseractEngine Engine)
|
||||
{
|
||||
//Kind of a big method but it has a lot of error handling and has to work with three special cases
|
||||
string rawResult = "";
|
||||
int result = 0;
|
||||
|
||||
//Debug
|
||||
int salt = rnd.Next(0, 999999);
|
||||
|
||||
switch (windowType)
|
||||
{
|
||||
case OcrImage.WindowType.Sector:
|
||||
@@ -113,7 +153,7 @@ namespace Test_Merge
|
||||
}
|
||||
|
||||
|
||||
Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType);
|
||||
Bitmap enhancedImage = new OcrImage(image).Enhance(windowType);
|
||||
|
||||
var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage));
|
||||
|
||||
@@ -149,50 +189,378 @@ namespace Test_Merge
|
||||
//removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian)
|
||||
rawNumbers.RemoveAll(x => ((string)x) == "");
|
||||
|
||||
if (rawNumbers.Count == 3)
|
||||
int minuts = 0;
|
||||
int seconds = 0;
|
||||
int miliseconds = 0;
|
||||
switch (windowType)
|
||||
{
|
||||
//mm:ss:ms
|
||||
result = (Convert.ToInt32(rawNumbers[0]) * 1000 * 60) + (Convert.ToInt32(rawNumbers[1]) * 1000) + Convert.ToInt32(rawNumbers[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers.Count == 2)
|
||||
{
|
||||
//ss:ms
|
||||
result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]);
|
||||
|
||||
if (result > 999999)
|
||||
{
|
||||
//We know that we have way too much seconds to make a minut
|
||||
//Its usually because the ":" have been interpreted as a number
|
||||
int minuts = (int)(rawNumbers[0][0] - '0');
|
||||
// rawNumbers[0][1] should contain the : that has been mistaken
|
||||
int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
|
||||
int ms = Convert.ToInt32(rawNumbers[1]);
|
||||
result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers.Count == 1)
|
||||
case OcrImage.WindowType.Sector:
|
||||
//Usually there is supposed to be only 2 parts.
|
||||
if (rawNumbers.Count == 2)
|
||||
{
|
||||
//The perect case
|
||||
try
|
||||
{
|
||||
result = Convert.ToInt32(rawNumbers[0]);
|
||||
seconds = Convert.ToInt32(rawNumbers[0].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[1].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
//It can be because the input is empty or because its the LEADER bracket
|
||||
result = 0;
|
||||
Console.WriteLine("Sector time convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Auuuugh
|
||||
if (rawNumbers.Count == 1)
|
||||
{
|
||||
//Here it is a little harder... Usually its because a '.' has been overlooked or interpreted as a number
|
||||
|
||||
if (rawNumbers[0].Length == 6)
|
||||
{
|
||||
//The '.' has been understood as a number
|
||||
try
|
||||
{
|
||||
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Sector time convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers[0].Length == 5)
|
||||
{
|
||||
//The '.' has been overlooked
|
||||
try
|
||||
{
|
||||
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Sector time convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Sector time convertion failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//The OCR detected more than 1 '.' wich is concerning because that means that something went really wrong
|
||||
Console.WriteLine("Sector time convertion failed");
|
||||
}
|
||||
}
|
||||
|
||||
result = 0;
|
||||
result += seconds * 1000;
|
||||
result += miliseconds;
|
||||
break;
|
||||
case OcrImage.WindowType.LapTime:
|
||||
|
||||
if (rawNumbers.Count == 3)
|
||||
{
|
||||
//The normal way
|
||||
try
|
||||
{
|
||||
minuts = Convert.ToInt32(rawNumbers[0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[1].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[2].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers.Count == 2)
|
||||
{
|
||||
//Either the ':' or the '.' has been missinterpreted
|
||||
if (rawNumbers[0].Length > rawNumbers[1].Length)
|
||||
{
|
||||
//The ':' has been missinterpreted
|
||||
if (rawNumbers[0].Length == 3)
|
||||
{
|
||||
//It has been forgotten
|
||||
try
|
||||
{
|
||||
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[1]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers[0].Length == 4)
|
||||
{
|
||||
//I has been translated into an other number
|
||||
try
|
||||
{
|
||||
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[1]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//This could happen if the ':' has been missinterpreted with a lap time of over 9 minuts (HIGLY IMPROBABLE)
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//The '.' has been missinterpreted
|
||||
if (rawNumbers[1].Length == 5)
|
||||
{
|
||||
//It has been forgotten
|
||||
minuts = Convert.ToInt32(rawNumbers[0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[1][0].ToString() + rawNumbers[1][1].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[1][2].ToString() + rawNumbers[1][3].ToString() + rawNumbers[1][4].ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers[1].Length == 6)
|
||||
{
|
||||
try
|
||||
{
|
||||
//It has been interpreted as a number
|
||||
minuts = Convert.ToInt32(rawNumbers[0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[1][0].ToString() + rawNumbers[1][1].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[1][3].ToString() + rawNumbers[1][4].ToString() + rawNumbers[1][5].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
//It can happen and to be honest I dont know how to fix it
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers.Count == 1)
|
||||
{
|
||||
//Both the '.' and the ':' have been missinterpreted
|
||||
if (rawNumbers[0].Length == 6)
|
||||
{
|
||||
//The just all have been forgotten
|
||||
try
|
||||
{
|
||||
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers[0].Length == 7)
|
||||
{
|
||||
//The '.' or ':' have been interpreted as a number (usually the ':')
|
||||
try
|
||||
{
|
||||
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString() + rawNumbers[0][6].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers[0].Length == 8)
|
||||
{
|
||||
//Both have been interpreted as a number
|
||||
try
|
||||
{
|
||||
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[0][5].ToString() + rawNumbers[0][6].ToString() + rawNumbers[0][7].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//I dont know what could have happened
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//I dont know what could have happened
|
||||
Console.WriteLine("Lap time convertion failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = 0;
|
||||
result += minuts * 60 * 1000;
|
||||
result += seconds * 1000;
|
||||
result += miliseconds;
|
||||
break;
|
||||
case OcrImage.WindowType.Gap:
|
||||
if (rawNumbers.Count == 2)
|
||||
{
|
||||
// This should be the x.xxx or a missed x:xx.xxx
|
||||
if (rawNumbers[0].Length > 2)
|
||||
{
|
||||
//Its a missed x:xx.xxx
|
||||
if (rawNumbers[0].Length == 3)
|
||||
{
|
||||
//It forgot the ":"
|
||||
try
|
||||
{
|
||||
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[1]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Gap to leader convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//The ":" has been mistaken as a number
|
||||
if (rawNumbers[0].Length == 4)
|
||||
{
|
||||
try
|
||||
{
|
||||
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[1]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Gap to leader convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Gap to leader convertion failed");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//It should be a normal x.xxx or xx.xxx
|
||||
try
|
||||
{
|
||||
seconds = Convert.ToInt32(rawNumbers[0].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[1].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Gap to leader convertion failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers.Count == 1)
|
||||
{
|
||||
//can be anything depending on the size of the string
|
||||
if (rawNumbers[0].Length == 4)
|
||||
{
|
||||
//We just missed the '.'
|
||||
try
|
||||
{
|
||||
seconds = Convert.ToInt32(rawNumbers[0][0].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Gap to leader convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers[0].Length == 5)
|
||||
{
|
||||
//We just missed the '.'
|
||||
try
|
||||
{
|
||||
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Gap to leader convertion failed");
|
||||
}
|
||||
}
|
||||
//There is just too much possibilities that it would be stupid to try and tell them appart so for now im leaving that as just an error
|
||||
Console.WriteLine("Gap to leader convertion failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rawNumbers.Count == 3)
|
||||
{
|
||||
// This should be the x:xx.xxx
|
||||
try
|
||||
{
|
||||
//Gaps cant be more than 9 minuts so if there is more than 1 digit it means that the '+' has been understood as an other number
|
||||
if (rawNumbers[0].Length > 1)
|
||||
rawNumbers[0] = rawNumbers[0][rawNumbers[0].Length - 1].ToString();
|
||||
|
||||
minuts = Convert.ToInt32(rawNumbers[0].ToString());
|
||||
seconds = Convert.ToInt32(rawNumbers[1].ToString());
|
||||
miliseconds = Convert.ToInt32(rawNumbers[2].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Gap to leader convertion failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result = 0;
|
||||
result += minuts * 60 * 1000;
|
||||
result += seconds * 1000;
|
||||
result += miliseconds;
|
||||
break;
|
||||
default:
|
||||
try
|
||||
{
|
||||
result = Convert.ToInt32(rawNumbers[0].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
page.Dispose();
|
||||
return result;
|
||||
}
|
||||
@@ -204,13 +572,16 @@ namespace Test_Merge
|
||||
/// <param name="allowedChars">The list of allowed chars</param>
|
||||
/// <param name="windowType">The type of window the text is on. Depending on the context the OCR will behave differently</param>
|
||||
/// <returns>the string it found</returns>
|
||||
public static async Task<string> GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = "", OcrImage.WindowType windowType = OcrImage.WindowType.Text)
|
||||
public static string GetStringFromPng(Bitmap image, TesseractEngine Engine, string allowedChars = "", OcrImage.WindowType windowType = OcrImage.WindowType.Text)
|
||||
{
|
||||
string result = "";
|
||||
|
||||
//Debug
|
||||
int salt = rnd.Next(0, 999999);
|
||||
|
||||
Engine.SetVariable("tessedit_char_whitelist", allowedChars);
|
||||
|
||||
Bitmap rawData = WindowImage;
|
||||
Bitmap rawData = image;
|
||||
Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType);
|
||||
|
||||
Page page = Engine.Process(enhancedImage);
|
||||
@@ -301,17 +672,5 @@ namespace Test_Merge
|
||||
|
||||
return d[string1.Length, string2.Length];
|
||||
}
|
||||
public virtual string ToJSON()
|
||||
{
|
||||
string result = "";
|
||||
|
||||
result += "\"" + Name + "\"" + ":{" + Environment.NewLine;
|
||||
result += "\t" + "\"x\":" + Bounds.X + "," + Environment.NewLine;
|
||||
result += "\t" + "\"y\":" + Bounds.Y + "," + Environment.NewLine;
|
||||
result += "\t" + "\"width\":" + Bounds.Width + Environment.NewLine;
|
||||
result += "}";
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user