using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using foodmarket.Pos.ViewModels;
using foodmarket.Pos.Views;
namespace foodmarket.Pos.Tooling;
/// Headless-рендер экранов кассы в PNG (без дисплея, Skia). По порядку
/// сценария, в 1920×1080 и 1366×768 → docs/pos-screenshots/ + index.html.
/// Запуск: dotnet run -- --screenshots [outputDir]
public static class ScreenshotRunner
{
private sealed record Shot(string Name, string Caption, Func Build);
public static int Run(string[] args)
{
var outDir = args.SkipWhile(a => a != "--screenshots").Skip(1).FirstOrDefault(a => !a.StartsWith("--"))
?? Path.Combine(FindRepoRoot(), "docs", "pos-screenshots");
Directory.CreateDirectory(outDir);
AppBuilder.Configure()
.UseSkia()
.UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false })
.WithInterFont()
.SetupWithoutStarting();
var shots = new List
{
new("01-login", "Вход кассира (адрес сервера, логин, пароль)", () => new LoginView { DataContext = LoginViewModel.Sample() }),
new("01b-login-pin", "Быстрый вход по PIN (зарегистрированное устройство)", () => new LoginView { DataContext = LoginViewModel.SamplePin() }),
new("02-shift-open", "Смена: Z-отчёт/состояние смены", () => new ShiftView { DataContext = ShiftViewModel.Sample() }),
new("03-sale-empty", "Продажа: пустой чек, ожидание сканирования", () => new SaleView { DataContext = new SaleViewModel() }),
new("04-sale-items", "Продажа: чек с товарами (вкл. весовой)", () => new SaleView { DataContext = SaleViewModel.Sample() }),
new("05-payment-cash", "Оплата наличными со сдачей", () => new PaymentView { DataContext = PaymentViewModel.SampleCash() }),
new("06-payment-mixed", "Оплата смешанная (нал+карта)", () => new PaymentView { DataContext = PaymentViewModel.SampleMixed() }),
new("07-return", "Возврат по чеку (выбор позиций)", () => new ReturnView { DataContext = ReturnViewModel.Sample() }),
new("08-shift-z", "Z-отчёт при закрытии (должно/факт/разница)", () => new ShiftView { DataContext = ShiftViewModel.SampleClosed() }),
new("09-shell-offline", "Статус-бар: нет связи, чеки в очереди", () => ShellOffline()),
new("10-shell-online", "Статус-бар: на связи, очередь пуста", () => ShellOnline()),
};
var sizes = new (string suffix, int w, int h)[] { ("1920x1080", 1920, 1080), ("1366x768", 1366, 768) };
var produced = new List<(string file, string caption)>();
foreach (var shot in shots)
foreach (var (suffix, w, h) in sizes)
{
var control = shot.Build();
// Если это уже окно (оболочка) — рендерим его напрямую; иначе кладём в окно.
var window = control as Window ?? new Window { Content = control, SystemDecorations = SystemDecorations.None };
window.Width = w; window.Height = h;
window.Show();
Dispatcher.UIThread.RunJobs();
// дать разметке/рендеру отработать
for (int i = 0; i < 3; i++) { Dispatcher.UIThread.RunJobs(); }
var frame = window.CaptureRenderedFrame();
var file = $"{shot.Name}-{suffix}.png";
frame?.Save(Path.Combine(outDir, file));
produced.Add((file, $"{shot.Caption} ({suffix})"));
window.Close();
Console.WriteLine($"[shot] {file}");
}
WriteGallery(outDir, produced);
Console.WriteLine($"[done] {produced.Count} кадров → {outDir}");
return 0;
}
private static Control ShellOffline()
{
var vm = new MainWindowViewModel { IsOnline = false, QueueCount = 3, CashierName = "Пётр Кассиров", ShiftNumber = "С-2026-000001" };
return new MainWindow { DataContext = vm };
}
private static Control ShellOnline()
{
var vm = new MainWindowViewModel { IsOnline = true, QueueCount = 0, CashierName = "Пётр Кассиров", ShiftNumber = "С-2026-000001" };
return new MainWindow { DataContext = vm };
}
private static void WriteGallery(string dir, List<(string file, string caption)> shots)
{
var sb = new StringBuilder();
sb.Append("Касса food-market — скриншоты");
sb.Append("");
sb.Append($"
Касса food-market — скриншоты процессов
рендер headless (Avalonia/Skia, Linux) — 1920×1080 и 1366×768. Кадров: {shots.Count}
");
foreach (var (file, caption) in shots)
sb.Append($"{System.Net.WebUtility.HtmlEncode(caption)}");
sb.Append("
");
// Раздел «Windows 11» — реальные скриншоты окна на Windows (если есть).
var winDir = Path.Combine(dir, "windows");
if (Directory.Exists(winDir))
{
var wins = Directory.GetFiles(winDir, "*.png").OrderBy(f => f).ToList();
if (wins.Count > 0)
{
sb.Append("
Windows 11 — реальное окно
" +
"
Снято на Windows 11 (VM, " +
"software-рендер — у VM нет GPU; консоль 1280×800). Подтверждает нативный " +
"рендер: кириллица, формат чисел «3 362 ₸», кнопки без обрезки. Высокое " +
"разрешение 1366/1920 — в headless-кадрах выше (тот же движок Avalonia/Skia/Inter).
");
sb.Append("
");
foreach (var w in wins)
{
var rel = "windows/" + Path.GetFileName(w);
sb.Append($"Windows 11 — экран продажи (реальное окно)");
}
sb.Append("
");
}
}
sb.Append("");
File.WriteAllText(Path.Combine(dir, "index.html"), sb.ToString());
}
private static string FindRepoRoot()
{
var d = new DirectoryInfo(AppContext.BaseDirectory);
while (d is not null && !File.Exists(Path.Combine(d.FullName, "food-market.sln"))) d = d.Parent;
return d?.FullName ?? Directory.GetCurrentDirectory();
}
}