add a POS895 target to EpsonTest that prints via the IPrinter adapter
EpsonTest predates the adapter layer: PrintCommandsToEpsonPrinterAsync takes an EpsonPrinter and drives it directly, so PrinterWrapperFactory, the model adapters and the 0x20 dispatch were never exercised. Against a POS895 that path feeds with ESC d, which the firmware ignores, so every cut slices through the last printed line, and it never sends a logo at all - the only logo branch is the HTML one, which uses GS ( L, the command that prints nothing on this hardware. PrintCommandsViaAdapterAsync instead does what EpsonPrintService does for a real job: query GS I 1, let PrinterWrapperFactory choose the adapter from the reported byte, then print the logo and commands through it and cut. It logs the reported id and the chosen adapter, so a wrong or missing model byte is visible rather than silent. Menu gains a third target and the non-interactive form accepts "pos895", so `dotnet run -- final pos895` prints a Final receipt with its logo through the production path. That covers the one part of the POS895 work nothing had executed end to end: the ESC * bit-image logo. Note EpsonTest must be run from its own directory; the logo paths are relative and `dotnet run --project` from the repo root cannot resolve them. That predates this change. Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
This commit is contained in:
@@ -18,8 +18,8 @@ if (args.Length > 0)
|
||||
return;
|
||||
}
|
||||
|
||||
int pt = args.Length > 1 && args[1].Equals("epson", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||
Console.WriteLine($"Running {GetReceiptTypeName(rt)} on {(pt == 0 ? "HTML" : "Epson")} printer...\n");
|
||||
int pt = ParsePrinterTargetArg(args.Length > 1 ? args[1] : "html");
|
||||
Console.WriteLine($"Running {GetReceiptTypeName(rt)} on {GetPrinterTargetName(pt)} printer...\n");
|
||||
await RunTest(rt, pt);
|
||||
Console.WriteLine("\nDone.");
|
||||
return;
|
||||
@@ -46,11 +46,12 @@ while (true)
|
||||
var printerTarget = ShowMenu("Select printer target:", new[]
|
||||
{
|
||||
"HTML Printer (local file)",
|
||||
"Epson Printer (real hardware)"
|
||||
"Epson Printer (real hardware)",
|
||||
"POS895 (real hardware, via IPrinter adapter)"
|
||||
});
|
||||
|
||||
Console.Clear();
|
||||
Console.WriteLine($"Running {GetReceiptTypeName(receiptType)} on {(printerTarget == 0 ? "HTML" : "Epson")} printer...\n");
|
||||
Console.WriteLine($"Running {GetReceiptTypeName(receiptType)} on {GetPrinterTargetName(printerTarget)} printer...\n");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -109,6 +110,21 @@ static int ShowMenu(string prompt, string[] options)
|
||||
}
|
||||
}
|
||||
|
||||
static string GetPrinterTargetName(int index) => index switch
|
||||
{
|
||||
0 => "HTML",
|
||||
1 => "Epson",
|
||||
2 => "POS895",
|
||||
_ => "Unknown"
|
||||
};
|
||||
|
||||
static int ParsePrinterTargetArg(string arg) => arg.ToLowerInvariant() switch
|
||||
{
|
||||
"1" or "epson" => 1,
|
||||
"2" or "pos895" or "pos" => 2,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
static string GetReceiptTypeName(int index) => index switch
|
||||
{
|
||||
0 => "KitchenReceipt",
|
||||
@@ -163,11 +179,19 @@ static async Task RunKitchenReceiptTest(int printerTarget)
|
||||
await TestHelpers.PrintKitchenCommandsToHtmlPrinterAsync(printer, printCommands);
|
||||
Console.WriteLine("Output written to: kitchen_test.html");
|
||||
}
|
||||
else
|
||||
else if (printerTarget == 1)
|
||||
{
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync(paperWidth: TestHelpers.KitchenPaperWidth);
|
||||
await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands, useDelay: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Routes through PrinterWrapperFactory and the model adapter, the way
|
||||
// EpsonPrintService does for a real job. The Epson branch above drives
|
||||
// EpsonPrinter directly and feeds with ESC d, which the POS895 ignores.
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RunFinalReceiptTest(int printerTarget)
|
||||
@@ -189,11 +213,20 @@ static async Task RunFinalReceiptTest(int printerTarget)
|
||||
logoPath: "ristorante-klinglers.ch-logo_white_bg.png");
|
||||
Console.WriteLine("Output written to: test.html");
|
||||
}
|
||||
else
|
||||
else if (printerTarget == 1)
|
||||
{
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Routes through PrinterWrapperFactory and the model adapter, the way
|
||||
// EpsonPrintService does for a real job. The Epson branch above drives
|
||||
// EpsonPrinter directly and feeds with ESC d, which the POS895 ignores.
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands,
|
||||
logoPath: "ristorante-klinglers.ch-logo_white_bg.png");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RunInvoiceReceiptTest(int printerTarget)
|
||||
@@ -215,11 +248,20 @@ static async Task RunInvoiceReceiptTest(int printerTarget)
|
||||
logoPath: "ristorante-klinglers.ch-logo_white_bg.png");
|
||||
Console.WriteLine("Output written to: invoice_test.html");
|
||||
}
|
||||
else
|
||||
else if (printerTarget == 1)
|
||||
{
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Routes through PrinterWrapperFactory and the model adapter, the way
|
||||
// EpsonPrintService does for a real job. The Epson branch above drives
|
||||
// EpsonPrinter directly and feeds with ESC d, which the POS895 ignores.
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands,
|
||||
logoPath: "ristorante-klinglers.ch-logo_white_bg.png");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RunBarReceiptTest(int printerTarget)
|
||||
@@ -239,11 +281,19 @@ static async Task RunBarReceiptTest(int printerTarget)
|
||||
await TestHelpers.PrintKitchenCommandsToHtmlPrinterAsync(printer, printCommands);
|
||||
Console.WriteLine("Output written to: bar_test.html");
|
||||
}
|
||||
else
|
||||
else if (printerTarget == 1)
|
||||
{
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync(paperWidth: TestHelpers.KitchenPaperWidth);
|
||||
await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands, useDelay: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Routes through PrinterWrapperFactory and the model adapter, the way
|
||||
// EpsonPrintService does for a real job. The Epson branch above drives
|
||||
// EpsonPrinter directly and feeds with ESC d, which the POS895 ignores.
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RunOrderItemsTest(int printerTarget)
|
||||
@@ -262,11 +312,19 @@ static async Task RunOrderItemsTest(int printerTarget)
|
||||
await TestHelpers.PrintCommandsToHtmlPrinterAsync(printer, printCommands);
|
||||
Console.WriteLine("Output written to: order_items.html");
|
||||
}
|
||||
else
|
||||
else if (printerTarget == 1)
|
||||
{
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Routes through PrinterWrapperFactory and the model adapter, the way
|
||||
// EpsonPrintService does for a real job. The Epson branch above drives
|
||||
// EpsonPrinter directly and feeds with ESC d, which the POS895 ignores.
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RunMoneyReturnReceiptTest(int printerTarget)
|
||||
@@ -288,9 +346,18 @@ static async Task RunMoneyReturnReceiptTest(int printerTarget)
|
||||
logoPath: "ristorante-klinglers.ch-logo_white_bg.png");
|
||||
Console.WriteLine("Output written to: money_return_test.html");
|
||||
}
|
||||
else
|
||||
else if (printerTarget == 1)
|
||||
{
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Routes through PrinterWrapperFactory and the model adapter, the way
|
||||
// EpsonPrintService does for a real job. The Epson branch above drives
|
||||
// EpsonPrinter directly and feeds with ESC d, which the POS895 ignores.
|
||||
var printer = await TestHelpers.CreateEpsonPrinterAsync();
|
||||
await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands,
|
||||
logoPath: "ristorante-klinglers.ch-logo_white_bg.png");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using Inspectron.Epson;
|
||||
using Inspectron.Epson.PrintServer.Printers;
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils.BarReceipt;
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils.FinalRecipe;
|
||||
@@ -577,6 +579,48 @@ public static class TestHelpers
|
||||
await printer.CutAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints through the production IPrinter adapter chain instead of driving
|
||||
/// EpsonPrinter directly: query the model byte, let PrinterWrapperFactory pick
|
||||
/// the adapter from it, then print the logo and commands through that adapter.
|
||||
/// This is what EpsonPrintService does for a real job, so it exercises the
|
||||
/// model dispatch and the adapter's own ESC/POS dialect.
|
||||
///
|
||||
/// Use this for the POS895. PrintCommandsToEpsonPrinterAsync above predates the
|
||||
/// adapter layer and feeds with ESC d, which that firmware ignores - every cut
|
||||
/// would slice through the last printed line.
|
||||
/// </summary>
|
||||
public static async Task PrintCommandsViaAdapterAsync(
|
||||
EpsonPrinter printer,
|
||||
IEnumerable<PrintCommand> commands,
|
||||
string? logoPath = null)
|
||||
{
|
||||
byte? printerId = await printer.GetPrinterIdAsync();
|
||||
if (printerId is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Printer did not answer GS I 1, so no adapter can be chosen for it.");
|
||||
}
|
||||
|
||||
Console.WriteLine($"Printer reported model id 0x{printerId.Value:X2}");
|
||||
|
||||
IPrinter adapter = new PrinterWrapperFactory()
|
||||
.CreatePrinterFromId(printerId.Value, printer);
|
||||
|
||||
Console.WriteLine($"Using adapter {adapter.GetType().Name}");
|
||||
|
||||
await adapter.InitAsync();
|
||||
|
||||
if (logoPath is not null)
|
||||
{
|
||||
Console.WriteLine($"Printing logo {logoPath}");
|
||||
await adapter.PrintImageAsync(logoPath);
|
||||
}
|
||||
|
||||
await adapter.PrintAsync(commands.ToList());
|
||||
await adapter.Cut();
|
||||
}
|
||||
|
||||
public static async Task PrintKitchenCommandsToHtmlPrinterAsync(
|
||||
HtmlPrinter printer,
|
||||
IEnumerable<PrintCommand> commands)
|
||||
|
||||
173
EpsonTest/test.html
Normal file
173
EpsonTest/test.html
Normal file
@@ -0,0 +1,173 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Epson Printer Output</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f0f0f0;
|
||||
padding: 20px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
.receipt {
|
||||
width: 384px;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.receipt span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.receipt .image {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.receipt .image img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cut {
|
||||
position: relative;
|
||||
border-top: 2px dashed #333;
|
||||
margin: 20px 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.cut .scissors {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: -12px;
|
||||
font-size: 18px;
|
||||
background: #fff;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
/* Simulated paper texture */
|
||||
.receipt::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 1px,
|
||||
rgba(0,0,0,0.02) 1px,
|
||||
rgba(0,0,0,0.02) 2px
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="receipt">
|
||||
<div class="image" style="margin-left: 42px;"><img src="test_images/image_0001.png" alt="Printed image"></div>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Klingler Gastro AG</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Münzplatz 3</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> CH-8001 Zürich</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> 043 321 22 22</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Debitorenrechnung</span>
|
||||
<span style="font-size: 12px; font-weight: bold; line-height: 13px;"> 14:58 16.12.2025</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Guests: 2</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;">1x San Pellegrino 50cl 6.50 6.50 A</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">1x Panna 50cl 6.50 6.50 A</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">1x An item with really long 5.50 5.50 A</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> description that should wrap</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">2x Business Lunch Menu Seco 44.00 88.00 A</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> - Tomato Soup</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> - Grilled Salmon</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> - Tiramisu</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">2x Brunello di Montalcino 1 16.00 32.00 A</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">2x Espresso 5.50 11.00 A</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">1x Tip 10.50 10.50 B</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> ---------</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; letter-spacing: 6px; font-weight: bold; line-height: 13px;"> Summe: 160.00 CHF</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> 172.80 EUR</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Fine Dine -50.00 CHF</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Cash: 50.00 CHF</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Card: 110.00 CHF</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; font-weight: bold; line-height: 13px;">MASTER 160.00 CHF</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;">MwSt % Brutto Netto MwSt</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">A: 8.1% 149.50 CHF 138.30 CHF 11.20 CHF</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">B: 0% 10.50 CHF 10.50 CHF 0.00 CHF</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Bedient von: Yves</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Terminal: Hauptkasse ZH</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Tisch: 12</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> CHE-449.635.880 MWST</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> *** Kundenbeleg ***</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Buchung</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> TWINT</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">XXXXXXXXXXXXXXX1494</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">11.11.2025 12:34:49</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Trm-Id: 31108834</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">AID: A0000015749E</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Trx. Seq-Cnt: 6127</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Trx. Ref-No: 99036644599</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Auth. Code: 0d3396</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Acq-Id: 2</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">EFT CHF: 67.00</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Trinkgeld CHF: 6.70</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Total-EFT CHF: 73.70</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">------------------------------------------------</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> *** Kundenbeleg ***</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Buchung</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> MASTER</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">XXXXXXXXXXXXXXX2587</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">11.11.2025 12:36:12</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Trm-Id: 31108834</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">AID: A0000000041010</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Trx. Seq-Cnt: 6128</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Trx. Ref-No: 99036644600</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Auth. Code: 1a4b2c</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Acq-Id: 1</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">EFT CHF: 93.00</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Trinkgeld CHF: 0.00</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Total-EFT CHF: 93.00</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">------------------------------------------------</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Das Team bedankt sich herzlich für Ihren</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Besuch.</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"> Auf Wiedersehen.</span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;"></span>
|
||||
<span style="font-size: 12px; line-height: 13px;">------------------------------------------------</span>
|
||||
<span style="font-size: 12px; line-height: 13px;">Unterschrift</span>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
EpsonTest/test_images/image_0001.png
Normal file
BIN
EpsonTest/test_images/image_0001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Reference in New Issue
Block a user