Implement stage 1 tray app scaffold
This commit is contained in:
@@ -1,52 +1,44 @@
|
||||
# LabelPrintAgent
|
||||
|
||||
Windows-Tray-Anwendung zum Rendern und Drucken von Etiketten aus JSON-Layouts.
|
||||
Windows-Tray-Anwendung für den späteren Etikettendruck aus JSON-Layouts.
|
||||
|
||||
## Stand
|
||||
## Etappe 1
|
||||
|
||||
Der erste lauffähige Schritt enthält:
|
||||
Dieser Stand ist ein lauffähiges Grundgerüst:
|
||||
|
||||
- .NET-9-Windows-Forms-Projekt mit Tray-Icon
|
||||
- Einstellungsdialog mit Tabs für Allgemein, Datenbank, Drucker, Layout und Fehlerjobs
|
||||
- .NET-9-Windows-Forms-Projekt
|
||||
- Tray-Icon mit Kontextmenü
|
||||
- Einstellungsdialog mit Tabs:
|
||||
- Allgemein
|
||||
- Datenbank
|
||||
- Drucker
|
||||
- Layout
|
||||
- Fehlerhafte Druckaufträge
|
||||
- lokale Konfiguration unter `C:\ProgramData\LabelPrintAgent\settings.json`
|
||||
- automatische Anlage von `C:\ProgramData\LabelPrintAgent`
|
||||
- Layout-Ordner unter `C:\ProgramData\LabelPrintAgent\layouts`
|
||||
- Log-Ordner unter `C:\ProgramData\LabelPrintAgent\logs`
|
||||
- verschlüsselte Passwortspeicherung per Windows DPAPI
|
||||
- Druckerauswahl über installierte Windows-Drucker
|
||||
- Layout-JSON unter `C:\ProgramData\LabelPrintAgent\layouts`
|
||||
- Auflistung installierter Windows-Drucker
|
||||
- Beispiel-Layout `dymo_57x32_standard`
|
||||
- Rendering mit 300 dpi als Bitmap
|
||||
- Vorschau und Testdruck über `PrintDocument`
|
||||
- Serilog-Dateilogs unter `C:\ProgramData\LabelPrintAgent\logs`
|
||||
- vorbereiteter MySQL-Worker ohne automatischen Retry
|
||||
- Layout-JSON laden, validieren und speichern
|
||||
|
||||
## Öffnen und Starten
|
||||
Noch nicht enthalten sind MySQL-Worker, echtes Drucken, QR-Code-Rendering und die vollständige Rendering-Engine.
|
||||
|
||||
## Startanleitung
|
||||
|
||||
1. `LabelPrintAgent.sln` in Visual Studio oder Rider öffnen.
|
||||
2. Auf einem Windows-Rechner bauen und starten.
|
||||
3. Tray-Symbol anklicken, um die Einstellungen zu öffnen.
|
||||
4. Im Tab `Drucker` den Dymo LabelWriter auswählen.
|
||||
5. Im Tab `Layout` das Beispiel-Layout öffnen, `Vorschau` klicken und danach `Testdruck`.
|
||||
3. Das Tray-Symbol anklicken oder per Kontextmenü `Einstellungen` öffnen.
|
||||
4. Im Tab `Drucker` einen installierten Windows-Drucker auswählen und speichern.
|
||||
5. Im Tab `Layout` das Beispiel-Layout prüfen, bearbeiten und speichern.
|
||||
|
||||
## Datenbank
|
||||
Beim ersten Start werden die Programmdatenordner und das Beispiel-Layout automatisch angelegt.
|
||||
|
||||
Die Tabelle kann mit folgendem Skript angelegt werden:
|
||||
## Spätere Etappen
|
||||
|
||||
```sql
|
||||
sql/create_label_print_queue.sql
|
||||
```
|
||||
|
||||
Der Worker lädt nur Jobs mit `status = 'pending'`. Vor dem Druck wird der Job auf `printing` gesetzt. Bei Erfolg setzt er `printed`, `printed_at` und `printer_name`. Bei Fehlern setzt er `error`, speichert `error_message` und erhöht `attempts`.
|
||||
|
||||
Fehlerhafte Jobs werden nicht automatisch wiederholt. Im Tab `Fehlerhafte Druckaufträge` können sie manuell erneut auf `pending` gesetzt oder als `deleted` markiert werden.
|
||||
|
||||
## Layout-Platzhalter
|
||||
|
||||
Text- und QR-Werte unterstützen Platzhalter aus `payload_json`:
|
||||
Die SQL-Datei für die spätere Druckwarteschlange liegt bereits unter:
|
||||
|
||||
```text
|
||||
{titel}
|
||||
{nummer}
|
||||
{datum:dd.MM.yyyy}
|
||||
{menge:0.00}
|
||||
sql/create_label_print_queue.sql
|
||||
```
|
||||
|
||||
Formatangaben werden über C#-Formatstrings ausgewertet.
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
using LabelPrintAgent.Configuration;
|
||||
using LabelPrintAgent.Layout;
|
||||
using LabelPrintAgent.Logging;
|
||||
using LabelPrintAgent.Rendering;
|
||||
using LabelPrintAgent.Printing;
|
||||
using LabelPrintAgent.Database;
|
||||
using LabelPrintAgent.Worker;
|
||||
using Serilog;
|
||||
|
||||
namespace LabelPrintAgent.App;
|
||||
@@ -26,13 +23,9 @@ internal static class Program
|
||||
LayoutStore.EnsureExampleLayout(settings.Paths.LayoutFolder);
|
||||
|
||||
using var layoutStore = new LayoutStore(settings.Paths.LayoutFolder);
|
||||
using var renderer = new LabelRenderer();
|
||||
var printerService = new PrinterService(new WindowsImagePrinter());
|
||||
var repository = new MySqlLabelRepository(settingsStore, protectedStrings);
|
||||
using var worker = new PrintJobWorker(settingsStore, layoutStore, renderer, printerService, repository);
|
||||
worker.Start();
|
||||
var printerService = new PrinterService();
|
||||
|
||||
Application.Run(new TrayApplicationContext(settingsStore, layoutStore, renderer, printerService, repository, worker));
|
||||
Application.Run(new TrayApplicationContext(settingsStore, layoutStore, printerService));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
using System.Drawing;
|
||||
using LabelPrintAgent.Configuration;
|
||||
using LabelPrintAgent.Database;
|
||||
using LabelPrintAgent.Layout;
|
||||
using LabelPrintAgent.Printing;
|
||||
using LabelPrintAgent.Rendering;
|
||||
using LabelPrintAgent.Worker;
|
||||
using Microsoft.Win32;
|
||||
using Serilog;
|
||||
|
||||
@@ -17,10 +14,7 @@ internal sealed class SettingsForm : Form
|
||||
|
||||
private readonly SettingsStore _settingsStore;
|
||||
private readonly LayoutStore _layoutStore;
|
||||
private readonly LabelRenderer _renderer;
|
||||
private readonly PrinterService _printerService;
|
||||
private readonly MySqlLabelRepository _repository;
|
||||
private readonly PrintJobWorker _worker;
|
||||
|
||||
private readonly CheckBox _autoStartCheckBox = new() { Text = "Autostart aktivieren", AutoSize = true };
|
||||
private readonly NumericUpDown _pollInterval = new() { Minimum = 1, Maximum = 3600, Value = 5, Width = 100 };
|
||||
@@ -37,8 +31,15 @@ internal sealed class SettingsForm : Form
|
||||
private readonly NumericUpDown _labelHeight = new() { Minimum = 1, Maximum = 500, DecimalPlaces = 1, Value = 32, Width = 100 };
|
||||
|
||||
private readonly ComboBox _layoutKeys = new() { Width = 280, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||
private readonly TextBox _layoutJson = new() { Multiline = true, ScrollBars = ScrollBars.Both, WordWrap = false, Font = new Font(FontFamily.GenericMonospace, 9), Width = 620, Height = 360 };
|
||||
private readonly PictureBox _preview = new() { BorderStyle = BorderStyle.FixedSingle, SizeMode = PictureBoxSizeMode.Zoom, Width = 420, Height = 240, BackColor = Color.White };
|
||||
private readonly TextBox _layoutJson = new()
|
||||
{
|
||||
Multiline = true,
|
||||
ScrollBars = ScrollBars.Both,
|
||||
WordWrap = false,
|
||||
Font = new Font(FontFamily.GenericMonospace, 9),
|
||||
Width = 900,
|
||||
Height = 460
|
||||
};
|
||||
|
||||
private readonly DataGridView _errorJobs = new()
|
||||
{
|
||||
@@ -47,28 +48,18 @@ internal sealed class SettingsForm : Form
|
||||
AllowUserToDeleteRows = false,
|
||||
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
|
||||
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||
MultiSelect = false,
|
||||
Dock = DockStyle.Fill
|
||||
};
|
||||
|
||||
public SettingsForm(
|
||||
SettingsStore settingsStore,
|
||||
LayoutStore layoutStore,
|
||||
LabelRenderer renderer,
|
||||
PrinterService printerService,
|
||||
MySqlLabelRepository repository,
|
||||
PrintJobWorker worker)
|
||||
public SettingsForm(SettingsStore settingsStore, LayoutStore layoutStore, PrinterService printerService)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_layoutStore = layoutStore;
|
||||
_renderer = renderer;
|
||||
_printerService = printerService;
|
||||
_repository = repository;
|
||||
_worker = worker;
|
||||
|
||||
Text = "LabelPrintAgent Einstellungen";
|
||||
Width = 1120;
|
||||
Height = 760;
|
||||
Width = 1040;
|
||||
Height = 720;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
var tabs = new TabControl { Dock = DockStyle.Fill };
|
||||
@@ -79,12 +70,12 @@ internal sealed class SettingsForm : Form
|
||||
tabs.TabPages.Add(CreateErrorJobsTab());
|
||||
Controls.Add(tabs);
|
||||
|
||||
Load += async (_, _) =>
|
||||
Load += (_, _) =>
|
||||
{
|
||||
LoadSettingsIntoUi();
|
||||
LoadPrinters();
|
||||
LoadLayouts();
|
||||
await RefreshErrorJobsAsync();
|
||||
LoadEmptyErrorJobsTable();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,8 +97,8 @@ internal sealed class SettingsForm : Form
|
||||
panel.Controls.Add(Row(100, Label("Datenbank", 120), _database));
|
||||
panel.Controls.Add(Row(140, Label("Benutzer", 120), _username));
|
||||
panel.Controls.Add(Row(180, Label("Passwort", 120), _password));
|
||||
panel.Controls.Add(ButtonAt("Verbindung testen", 140, 230, async (_, _) => await TestConnectionAsync()));
|
||||
panel.Controls.Add(ButtonAt("Speichern", 310, 230, (_, _) => SaveDatabase()));
|
||||
panel.Controls.Add(ButtonAt("Speichern", 140, 230, (_, _) => SaveDatabase()));
|
||||
panel.Controls.Add(LabelAt("Verbindungstest folgt in Etappe 2.", 140, 276, 340));
|
||||
return new TabPage("Datenbank") { Controls = { panel } };
|
||||
}
|
||||
|
||||
@@ -118,10 +109,8 @@ internal sealed class SettingsForm : Form
|
||||
panel.Controls.Add(Row(60, Label("Breite", 120), _labelWidth, Label("mm", 40)));
|
||||
panel.Controls.Add(Row(100, Label("Höhe", 120), _labelHeight, Label("mm", 40)));
|
||||
panel.Controls.Add(ButtonAt("Drucker neu laden", 140, 150, (_, _) => LoadPrinters()));
|
||||
panel.Controls.Add(ButtonAt("Testdruck", 300, 150, (_, _) => PrintCurrentLayout()));
|
||||
panel.Controls.Add(_preview);
|
||||
_preview.Left = 520;
|
||||
_preview.Top = 20;
|
||||
panel.Controls.Add(ButtonAt("Speichern", 300, 150, (_, _) => SavePrinter()));
|
||||
panel.Controls.Add(LabelAt("Testdruck folgt in Etappe 2.", 140, 198, 340));
|
||||
return new TabPage("Drucker") { Controls = { panel } };
|
||||
}
|
||||
|
||||
@@ -133,26 +122,19 @@ internal sealed class SettingsForm : Form
|
||||
panel.Controls.Add(_layoutJson);
|
||||
_layoutJson.Left = 20;
|
||||
_layoutJson.Top = 60;
|
||||
panel.Controls.Add(_preview);
|
||||
_preview.Left = 680;
|
||||
_preview.Top = 60;
|
||||
panel.Controls.Add(ButtonAt("Validieren", 20, 440, (_, _) => ValidateLayout()));
|
||||
panel.Controls.Add(ButtonAt("Speichern", 130, 440, (_, _) => SaveLayout()));
|
||||
panel.Controls.Add(ButtonAt("Vorschau", 240, 440, (_, _) => RenderPreview()));
|
||||
panel.Controls.Add(ButtonAt("Testdruck", 350, 440, (_, _) => PrintCurrentLayout()));
|
||||
panel.Controls.Add(ButtonAt("Validieren", 20, 540, (_, _) => ValidateLayout()));
|
||||
panel.Controls.Add(ButtonAt("Speichern", 130, 540, (_, _) => SaveLayout()));
|
||||
panel.Controls.Add(LabelAt("Vorschau und Rendering folgen in Etappe 2.", 250, 546, 360));
|
||||
return new TabPage("Layout") { Controls = { panel } };
|
||||
}
|
||||
|
||||
private TabPage CreateErrorJobsTab()
|
||||
{
|
||||
var page = new TabPage("Fehlerhafte Druckaufträge");
|
||||
var panel = new Panel { Dock = DockStyle.Bottom, Height = 56, Padding = new Padding(12) };
|
||||
panel.Controls.Add(ButtonAt("Aktualisieren", 12, 12, async (_, _) => await RefreshErrorJobsAsync()));
|
||||
panel.Controls.Add(ButtonAt("Vorschau", 140, 12, (_, _) => PreviewSelectedErrorJob()));
|
||||
panel.Controls.Add(ButtonAt("Nochmal drucken", 240, 12, async (_, _) => await RequeueSelectedErrorJobAsync()));
|
||||
panel.Controls.Add(ButtonAt("Löschen/verwerfen", 390, 12, async (_, _) => await DeleteSelectedErrorJobAsync()));
|
||||
var footer = new Panel { Dock = DockStyle.Bottom, Height = 48, Padding = new Padding(12) };
|
||||
footer.Controls.Add(LabelAt("Fehlerhafte Jobs werden angezeigt, sobald der MySQL-Worker umgesetzt ist.", 12, 14, 600));
|
||||
page.Controls.Add(_errorJobs);
|
||||
page.Controls.Add(panel);
|
||||
page.Controls.Add(footer);
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -219,7 +201,6 @@ internal sealed class SettingsForm : Form
|
||||
try
|
||||
{
|
||||
_layoutJson.Text = _layoutStore.LoadJsonByKey(key);
|
||||
RenderPreview();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -227,6 +208,14 @@ internal sealed class SettingsForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadEmptyErrorJobsTable()
|
||||
{
|
||||
_errorJobs.DataSource = new[]
|
||||
{
|
||||
new ErrorJobRow(0, string.Empty, string.Empty, 0)
|
||||
};
|
||||
}
|
||||
|
||||
private void SaveGeneral()
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
@@ -248,20 +237,6 @@ internal sealed class SettingsForm : Form
|
||||
MessageBox.Show("Datenbankeinstellungen gespeichert.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private async Task TestConnectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
SaveDatabase();
|
||||
await _repository.TestConnectionAsync();
|
||||
MessageBox.Show("Verbindung erfolgreich.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void SavePrinter()
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
@@ -269,6 +244,7 @@ internal sealed class SettingsForm : Form
|
||||
settings.Printer.LabelWidthMm = _labelWidth.Value;
|
||||
settings.Printer.LabelHeightMm = _labelHeight.Value;
|
||||
_settingsStore.Save(settings);
|
||||
MessageBox.Show("Druckereinstellungen gespeichert.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private void ValidateLayout()
|
||||
@@ -298,115 +274,6 @@ internal sealed class SettingsForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderPreview()
|
||||
{
|
||||
try
|
||||
{
|
||||
var layout = _layoutStore.ValidateJson(_layoutJson.Text);
|
||||
using var result = _renderer.Render(layout, LayoutStore.ExamplePayloadJson);
|
||||
var old = _preview.Image;
|
||||
_preview.Image = (Bitmap)result.Bitmap.Clone();
|
||||
old?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintCurrentLayout()
|
||||
{
|
||||
try
|
||||
{
|
||||
SavePrinter();
|
||||
var layout = _layoutStore.ValidateJson(_layoutJson.Text);
|
||||
var settings = _settingsStore.Load();
|
||||
using var result = _renderer.Render(layout, LayoutStore.ExamplePayloadJson);
|
||||
_printerService.Print(result.Bitmap, settings.Printer.PrinterName, layout.WidthMm, layout.HeightMm);
|
||||
MessageBox.Show("Testdruck wurde gesendet.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshErrorJobsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var jobs = await _repository.LoadErrorJobsAsync();
|
||||
_errorJobs.DataSource = jobs.Select(job => new ErrorJobRow(job)).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Could not load error jobs");
|
||||
_errorJobs.DataSource = Array.Empty<ErrorJobRow>();
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewSelectedErrorJob()
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = GetSelectedErrorJob();
|
||||
var layout = _layoutStore.LoadByKey(job.Layout);
|
||||
using var result = _renderer.Render(layout, job.PayloadJson);
|
||||
var previewForm = new Form { Text = $"Vorschau Job {job.Id}", Width = 640, Height = 420, StartPosition = FormStartPosition.CenterParent };
|
||||
previewForm.Controls.Add(new PictureBox
|
||||
{
|
||||
Image = (Bitmap)result.Bitmap.Clone(),
|
||||
Dock = DockStyle.Fill,
|
||||
SizeMode = PictureBoxSizeMode.Zoom,
|
||||
BackColor = Color.White
|
||||
});
|
||||
previewForm.Show(this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequeueSelectedErrorJobAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = GetSelectedErrorJob();
|
||||
await _repository.RequeueAsync(job.Id);
|
||||
await _worker.PollOnceAsync();
|
||||
await RefreshErrorJobsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSelectedErrorJobAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = GetSelectedErrorJob();
|
||||
await _repository.MarkDeletedAsync(job.Id);
|
||||
await RefreshErrorJobsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private ErrorJobRow GetSelectedErrorJob()
|
||||
{
|
||||
if (_errorJobs.CurrentRow?.DataBoundItem is ErrorJobRow row)
|
||||
{
|
||||
return row;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Bitte zuerst einen fehlerhaften Druckauftrag auswählen.");
|
||||
}
|
||||
|
||||
private bool IsAutostartEnabled()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(AutoStartRunKey, writable: false);
|
||||
@@ -430,6 +297,19 @@ internal sealed class SettingsForm : Form
|
||||
|
||||
private static Label Label(string text, int width) => new() { Text = text, Width = width, TextAlign = ContentAlignment.MiddleLeft };
|
||||
|
||||
private static Label LabelAt(string text, int left, int top, int width)
|
||||
{
|
||||
return new Label
|
||||
{
|
||||
Text = text,
|
||||
Left = left,
|
||||
Top = top,
|
||||
Width = width,
|
||||
Height = 24,
|
||||
TextAlign = ContentAlignment.MiddleLeft
|
||||
};
|
||||
}
|
||||
|
||||
private static FlowLayoutPanel Row(int top, params Control[] controls)
|
||||
{
|
||||
var row = new FlowLayoutPanel
|
||||
@@ -458,20 +338,5 @@ internal sealed class SettingsForm : Form
|
||||
MessageBox.Show(ex.Message, Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
|
||||
private sealed class ErrorJobRow
|
||||
{
|
||||
private readonly PrintJob _job;
|
||||
|
||||
public ErrorJobRow(PrintJob job)
|
||||
{
|
||||
_job = job;
|
||||
}
|
||||
|
||||
public long Id => _job.Id;
|
||||
public string Layout => _job.LayoutKey;
|
||||
public DateTime ErstelltAm => _job.CreatedAt;
|
||||
public string Fehler => _job.ErrorMessage ?? string.Empty;
|
||||
public int Versuche => _job.Attempts;
|
||||
public string PayloadJson => _job.PayloadJson;
|
||||
}
|
||||
private sealed record ErrorJobRow(long Id, string Layout, string Fehler, int Versuche);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using LabelPrintAgent.Configuration;
|
||||
using LabelPrintAgent.Database;
|
||||
using LabelPrintAgent.Layout;
|
||||
using LabelPrintAgent.Printing;
|
||||
using LabelPrintAgent.Rendering;
|
||||
using LabelPrintAgent.Worker;
|
||||
using Serilog;
|
||||
|
||||
namespace LabelPrintAgent.App;
|
||||
@@ -13,26 +10,17 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
private readonly NotifyIcon _notifyIcon;
|
||||
private readonly SettingsStore _settingsStore;
|
||||
private readonly LayoutStore _layoutStore;
|
||||
private readonly LabelRenderer _renderer;
|
||||
private readonly PrinterService _printerService;
|
||||
private readonly MySqlLabelRepository _repository;
|
||||
private readonly PrintJobWorker _worker;
|
||||
private SettingsForm? _settingsForm;
|
||||
|
||||
public TrayApplicationContext(
|
||||
SettingsStore settingsStore,
|
||||
LayoutStore layoutStore,
|
||||
LabelRenderer renderer,
|
||||
PrinterService printerService,
|
||||
MySqlLabelRepository repository,
|
||||
PrintJobWorker worker)
|
||||
PrinterService printerService)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_layoutStore = layoutStore;
|
||||
_renderer = renderer;
|
||||
_printerService = printerService;
|
||||
_repository = repository;
|
||||
_worker = worker;
|
||||
|
||||
var menu = new ContextMenuStrip();
|
||||
menu.Items.Add("Einstellungen", null, (_, _) => ShowSettings());
|
||||
@@ -60,7 +48,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
{
|
||||
if (_settingsForm is null || _settingsForm.IsDisposed)
|
||||
{
|
||||
_settingsForm = new SettingsForm(_settingsStore, _layoutStore, _renderer, _printerService, _repository, _worker);
|
||||
_settingsForm = new SettingsForm(_settingsStore, _layoutStore, _printerService);
|
||||
}
|
||||
|
||||
_settingsForm.Show();
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace LabelPrintAgent.Database;
|
||||
|
||||
public sealed class DbConnectionSettings
|
||||
{
|
||||
public string Host { get; set; } = string.Empty;
|
||||
public uint Port { get; set; } = 3306;
|
||||
public string Database { get; set; } = string.Empty;
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
using LabelPrintAgent.Configuration;
|
||||
using MySqlConnector;
|
||||
|
||||
namespace LabelPrintAgent.Database;
|
||||
|
||||
public sealed class MySqlLabelRepository
|
||||
{
|
||||
private readonly SettingsStore _settingsStore;
|
||||
private readonly ProtectedStringService _protectedStringService;
|
||||
|
||||
public MySqlLabelRepository(SettingsStore settingsStore, ProtectedStringService protectedStringService)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_protectedStringService = protectedStringService;
|
||||
}
|
||||
|
||||
public async Task TestConnectionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new MySqlConnection(BuildConnectionString());
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PrintJob>> LoadErrorJobsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new MySqlConnection(BuildConnectionString());
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = """
|
||||
SELECT id, layout_key, payload_json, status, printer_name, attempts, error_message, created_at, locked_at, printed_at
|
||||
FROM label_print_queue
|
||||
WHERE status = 'error'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100;
|
||||
""";
|
||||
|
||||
var jobs = new List<PrintJob>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
jobs.Add(ReadJob(reader));
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PrintJob>> LoadPendingJobsAsync(int limit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new MySqlConnection(BuildConnectionString());
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = """
|
||||
SELECT id, layout_key, payload_json, status, printer_name, attempts, error_message, created_at, locked_at, printed_at
|
||||
FROM label_print_queue
|
||||
WHERE status = 'pending'
|
||||
ORDER BY created_at ASC
|
||||
LIMIT @limit;
|
||||
""";
|
||||
command.Parameters.AddWithValue("@limit", limit);
|
||||
|
||||
var jobs = new List<PrintJob>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
jobs.Add(ReadJob(reader));
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
public async Task<bool> MarkPrintingAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var affectedRows = await ExecuteAsync("""
|
||||
UPDATE label_print_queue
|
||||
SET status = 'printing', locked_at = CURRENT_TIMESTAMP
|
||||
WHERE id = @id AND status = 'pending';
|
||||
""", id, cancellationToken);
|
||||
return affectedRows == 1;
|
||||
}
|
||||
|
||||
public async Task MarkPrintedAsync(long id, string printerName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ExecuteAsync("""
|
||||
UPDATE label_print_queue
|
||||
SET status = 'printed', printer_name = @printerName, printed_at = CURRENT_TIMESTAMP, error_message = NULL
|
||||
WHERE id = @id;
|
||||
""", id, cancellationToken, ("@printerName", printerName));
|
||||
}
|
||||
|
||||
public async Task MarkErrorAsync(long id, string errorMessage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ExecuteAsync("""
|
||||
UPDATE label_print_queue
|
||||
SET status = 'error', error_message = @errorMessage, attempts = attempts + 1
|
||||
WHERE id = @id;
|
||||
""", id, cancellationToken, ("@errorMessage", errorMessage));
|
||||
}
|
||||
|
||||
public async Task RequeueAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ExecuteAsync("""
|
||||
UPDATE label_print_queue
|
||||
SET status = 'pending', error_message = NULL, locked_at = NULL
|
||||
WHERE id = @id AND status = 'error';
|
||||
""", id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task MarkDeletedAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ExecuteAsync("""
|
||||
UPDATE label_print_queue
|
||||
SET status = 'deleted'
|
||||
WHERE id = @id AND status = 'error';
|
||||
""", id, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<int> ExecuteAsync(string sql, long id, CancellationToken cancellationToken, params (string Name, object Value)[] parameters)
|
||||
{
|
||||
await using var connection = new MySqlConnection(BuildConnectionString());
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
command.Parameters.AddWithValue("@id", id);
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
command.Parameters.AddWithValue(parameter.Name, parameter.Value);
|
||||
}
|
||||
|
||||
return await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private string BuildConnectionString()
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
var builder = new MySqlConnectionStringBuilder
|
||||
{
|
||||
Server = settings.Database.Host,
|
||||
Port = settings.Database.Port,
|
||||
Database = settings.Database.Database,
|
||||
UserID = settings.Database.Username,
|
||||
Password = _protectedStringService.Unprotect(settings.Database.EncryptedPassword),
|
||||
SslMode = MySqlSslMode.Preferred
|
||||
};
|
||||
return builder.ConnectionString;
|
||||
}
|
||||
|
||||
private static PrintJob ReadJob(MySqlDataReader reader)
|
||||
{
|
||||
return new PrintJob
|
||||
{
|
||||
Id = reader.GetInt64("id"),
|
||||
LayoutKey = reader.GetString("layout_key"),
|
||||
PayloadJson = reader.GetString("payload_json"),
|
||||
Status = reader.GetString("status"),
|
||||
PrinterName = reader.IsDBNull(reader.GetOrdinal("printer_name")) ? null : reader.GetString("printer_name"),
|
||||
Attempts = reader.GetInt32("attempts"),
|
||||
ErrorMessage = reader.IsDBNull(reader.GetOrdinal("error_message")) ? null : reader.GetString("error_message"),
|
||||
CreatedAt = reader.GetDateTime("created_at"),
|
||||
LockedAt = reader.IsDBNull(reader.GetOrdinal("locked_at")) ? null : reader.GetDateTime("locked_at"),
|
||||
PrintedAt = reader.IsDBNull(reader.GetOrdinal("printed_at")) ? null : reader.GetDateTime("printed_at")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace LabelPrintAgent.Database;
|
||||
|
||||
public sealed class PrintJob
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string LayoutKey { get; set; } = string.Empty;
|
||||
public string PayloadJson { get; set; } = "{}";
|
||||
public string Status { get; set; } = "pending";
|
||||
public string? PrinterName { get; set; }
|
||||
public int Attempts { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? LockedAt { get; set; }
|
||||
public DateTime? PrintedAt { get; set; }
|
||||
}
|
||||
@@ -10,11 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Printing;
|
||||
|
||||
namespace LabelPrintAgent.Printing;
|
||||
|
||||
public sealed class PrinterService
|
||||
{
|
||||
private readonly WindowsImagePrinter _printer;
|
||||
|
||||
public PrinterService(WindowsImagePrinter printer)
|
||||
{
|
||||
_printer = printer;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetInstalledPrinters()
|
||||
{
|
||||
return PrinterSettings.InstalledPrinters.Cast<string>().OrderBy(name => name).ToList();
|
||||
}
|
||||
|
||||
public void Print(Bitmap bitmap, string printerName, decimal widthMm, decimal heightMm)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(printerName))
|
||||
{
|
||||
throw new InvalidOperationException("Bitte zuerst einen Drucker auswählen.");
|
||||
}
|
||||
|
||||
_printer.Print(bitmap, printerName, widthMm, heightMm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Printing;
|
||||
|
||||
namespace LabelPrintAgent.Printing;
|
||||
|
||||
public sealed class WindowsImagePrinter
|
||||
{
|
||||
public void Print(Bitmap bitmap, string printerName, decimal widthMm, decimal heightMm)
|
||||
{
|
||||
using var document = new PrintDocument();
|
||||
document.PrinterSettings.PrinterName = printerName;
|
||||
if (!document.PrinterSettings.IsValid)
|
||||
{
|
||||
throw new InvalidOperationException($"Drucker '{printerName}' ist nicht verfügbar.");
|
||||
}
|
||||
|
||||
document.DocumentName = "LabelPrintAgent Testdruck";
|
||||
document.DefaultPageSettings.PaperSize = new PaperSize(
|
||||
"Label 57x32mm",
|
||||
MmToHundredthsOfInch(widthMm),
|
||||
MmToHundredthsOfInch(heightMm));
|
||||
document.DefaultPageSettings.Landscape = widthMm > heightMm;
|
||||
document.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0);
|
||||
document.PrintPage += (_, args) =>
|
||||
{
|
||||
if (args.Graphics is null)
|
||||
{
|
||||
throw new InvalidOperationException("Druckgrafik konnte nicht initialisiert werden.");
|
||||
}
|
||||
|
||||
args.Graphics.DrawImage(bitmap, args.PageBounds);
|
||||
args.HasMorePages = false;
|
||||
};
|
||||
|
||||
document.Print();
|
||||
}
|
||||
|
||||
private static int MmToHundredthsOfInch(decimal mm)
|
||||
{
|
||||
return (int)Math.Round((double)mm / 25.4d * 100d);
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Drawing.Text;
|
||||
using System.Text.Json;
|
||||
using LabelPrintAgent.Layout;
|
||||
using QRCoder;
|
||||
|
||||
namespace LabelPrintAgent.Rendering;
|
||||
|
||||
public sealed class LabelRenderer : IDisposable
|
||||
{
|
||||
public RenderResult Render(LabelLayout layout, string payloadJson)
|
||||
{
|
||||
using var payload = JsonDocument.Parse(payloadJson);
|
||||
var width = MmToPixels(layout.WidthMm, layout.Dpi);
|
||||
var height = MmToPixels(layout.HeightMm, layout.Dpi);
|
||||
var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
|
||||
bitmap.SetResolution(layout.Dpi, layout.Dpi);
|
||||
|
||||
using var graphics = Graphics.FromImage(bitmap);
|
||||
graphics.Clear(Color.White);
|
||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
|
||||
foreach (var element in layout.Elements)
|
||||
{
|
||||
switch (element)
|
||||
{
|
||||
case TextElement text:
|
||||
DrawText(graphics, text, payload.RootElement, layout.Dpi);
|
||||
break;
|
||||
case LineElement line:
|
||||
DrawLine(graphics, line, layout.Dpi);
|
||||
break;
|
||||
case RectangleElement rectangle:
|
||||
DrawRectangle(graphics, rectangle, layout.Dpi);
|
||||
break;
|
||||
case QrCodeElement qr:
|
||||
DrawQr(graphics, qr, payload.RootElement, layout.Dpi);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new RenderResult(bitmap);
|
||||
}
|
||||
|
||||
private static void DrawText(Graphics graphics, TextElement element, JsonElement payload, int dpi)
|
||||
{
|
||||
var text = TemplateFormatter.Format(element.Value, payload);
|
||||
var bounds = new RectangleF(
|
||||
MmToPixelsF(element.XMm, dpi),
|
||||
MmToPixelsF(element.YMm, dpi),
|
||||
MmToPixelsF(element.WidthMm, dpi),
|
||||
MmToPixelsF(element.HeightMm, dpi));
|
||||
var style = FontStyle.Regular;
|
||||
if (element.Bold) style |= FontStyle.Bold;
|
||||
if (element.Italic) style |= FontStyle.Italic;
|
||||
if (element.Underline) style |= FontStyle.Underline;
|
||||
|
||||
using var format = BuildStringFormat(element);
|
||||
var fontSize = element.FontSizePt;
|
||||
Font? font = null;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
font?.Dispose();
|
||||
font = new Font(element.FontFamily, fontSize, style, GraphicsUnit.Point);
|
||||
var measured = graphics.MeasureString(text, font, bounds.Size, format);
|
||||
if (!element.AutoShrink || fontSize <= element.MinFontSizePt || (measured.Width <= bounds.Width && measured.Height <= bounds.Height))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
fontSize -= 0.5f;
|
||||
}
|
||||
|
||||
using var brush = new SolidBrush(Color.Black);
|
||||
graphics.DrawString(text, font, brush, bounds, format);
|
||||
}
|
||||
finally
|
||||
{
|
||||
font?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static StringFormat BuildStringFormat(TextElement element)
|
||||
{
|
||||
var format = new StringFormat
|
||||
{
|
||||
Trimming = StringTrimming.EllipsisCharacter,
|
||||
FormatFlags = element.Multiline ? 0 : StringFormatFlags.NoWrap
|
||||
};
|
||||
|
||||
format.Alignment = element.HorizontalAlign.ToLowerInvariant() switch
|
||||
{
|
||||
"center" => StringAlignment.Center,
|
||||
"right" => StringAlignment.Far,
|
||||
_ => StringAlignment.Near
|
||||
};
|
||||
format.LineAlignment = element.VerticalAlign.ToLowerInvariant() switch
|
||||
{
|
||||
"middle" => StringAlignment.Center,
|
||||
"bottom" => StringAlignment.Far,
|
||||
_ => StringAlignment.Near
|
||||
};
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
private static void DrawLine(Graphics graphics, LineElement element, int dpi)
|
||||
{
|
||||
using var pen = new Pen(Color.Black, Math.Max(1, MmToPixelsF(element.StrokeWidthMm, dpi)));
|
||||
graphics.DrawLine(
|
||||
pen,
|
||||
MmToPixelsF(element.X1Mm, dpi),
|
||||
MmToPixelsF(element.Y1Mm, dpi),
|
||||
MmToPixelsF(element.X2Mm, dpi),
|
||||
MmToPixelsF(element.Y2Mm, dpi));
|
||||
}
|
||||
|
||||
private static void DrawRectangle(Graphics graphics, RectangleElement element, int dpi)
|
||||
{
|
||||
var rect = new RectangleF(
|
||||
MmToPixelsF(element.XMm, dpi),
|
||||
MmToPixelsF(element.YMm, dpi),
|
||||
MmToPixelsF(element.WidthMm, dpi),
|
||||
MmToPixelsF(element.HeightMm, dpi));
|
||||
if (element.Filled)
|
||||
{
|
||||
using var brush = new SolidBrush(Color.Black);
|
||||
graphics.FillRectangle(brush, rect);
|
||||
return;
|
||||
}
|
||||
|
||||
using var pen = new Pen(Color.Black, Math.Max(1, MmToPixelsF(element.StrokeWidthMm, dpi)));
|
||||
graphics.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
|
||||
}
|
||||
|
||||
private static void DrawQr(Graphics graphics, QrCodeElement element, JsonElement payload, int dpi)
|
||||
{
|
||||
var text = TemplateFormatter.Format(element.Value, payload);
|
||||
using var generator = new QRCodeGenerator();
|
||||
using var qrData = generator.CreateQrCode(text, QRCodeGenerator.ECCLevel.Q);
|
||||
using var qrCode = new QRCode(qrData);
|
||||
using var qrBitmap = qrCode.GetGraphic(12, Color.Black, Color.White, drawQuietZones: true);
|
||||
graphics.DrawImage(
|
||||
qrBitmap,
|
||||
MmToPixelsF(element.XMm, dpi),
|
||||
MmToPixelsF(element.YMm, dpi),
|
||||
MmToPixelsF(element.SizeMm, dpi),
|
||||
MmToPixelsF(element.SizeMm, dpi));
|
||||
}
|
||||
|
||||
private static int MmToPixels(decimal mm, int dpi) => (int)Math.Round((double)mm / 25.4d * dpi);
|
||||
|
||||
private static float MmToPixelsF(decimal mm, int dpi) => (float)((double)mm / 25.4d * dpi);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Drawing;
|
||||
|
||||
namespace LabelPrintAgent.Rendering;
|
||||
|
||||
public sealed class RenderResult : IDisposable
|
||||
{
|
||||
public RenderResult(Bitmap bitmap)
|
||||
{
|
||||
Bitmap = bitmap;
|
||||
}
|
||||
|
||||
public Bitmap Bitmap { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Bitmap.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LabelPrintAgent.Rendering;
|
||||
|
||||
public static partial class TemplateFormatter
|
||||
{
|
||||
public static string Format(string template, JsonElement payload)
|
||||
{
|
||||
return PlaceholderRegex().Replace(template, match =>
|
||||
{
|
||||
var field = match.Groups["field"].Value;
|
||||
var format = match.Groups["format"].Success ? match.Groups["format"].Value : null;
|
||||
|
||||
if (!payload.TryGetProperty(field, out var value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var typedValue = ConvertJsonValue(value);
|
||||
if (format is null)
|
||||
{
|
||||
return Convert.ToString(typedValue, CultureInfo.CurrentCulture) ?? string.Empty;
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0:" + format + "}", typedValue);
|
||||
});
|
||||
}
|
||||
|
||||
private static object? ConvertJsonValue(JsonElement value)
|
||||
{
|
||||
return value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String when DateTime.TryParse(value.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var date) => date,
|
||||
JsonValueKind.String => value.GetString(),
|
||||
JsonValueKind.Number when value.TryGetInt64(out var longValue) => longValue,
|
||||
JsonValueKind.Number when value.TryGetDecimal(out var decimalValue) => decimalValue,
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
_ => value.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\{(?<field>[A-Za-z0-9_]+)(:(?<format>[^}]+))?\}")]
|
||||
private static partial Regex PlaceholderRegex();
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
using LabelPrintAgent.Configuration;
|
||||
using LabelPrintAgent.Database;
|
||||
using LabelPrintAgent.Layout;
|
||||
using LabelPrintAgent.Printing;
|
||||
using LabelPrintAgent.Rendering;
|
||||
using Serilog;
|
||||
|
||||
namespace LabelPrintAgent.Worker;
|
||||
|
||||
public sealed class PrintJobWorker : IDisposable
|
||||
{
|
||||
private readonly SettingsStore _settingsStore;
|
||||
private readonly LayoutStore _layoutStore;
|
||||
private readonly LabelRenderer _renderer;
|
||||
private readonly PrinterService _printerService;
|
||||
private readonly MySqlLabelRepository _repository;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private Task? _task;
|
||||
|
||||
public PrintJobWorker(
|
||||
SettingsStore settingsStore,
|
||||
LayoutStore layoutStore,
|
||||
LabelRenderer renderer,
|
||||
PrinterService printerService,
|
||||
MySqlLabelRepository repository)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_layoutStore = layoutStore;
|
||||
_renderer = renderer;
|
||||
_printerService = printerService;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_task is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_task = Task.Run(() => RunAsync(_cancellationTokenSource.Token));
|
||||
}
|
||||
|
||||
private async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await PollOnceAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Print worker poll failed");
|
||||
}
|
||||
|
||||
var settings = _settingsStore.Load();
|
||||
await Task.Delay(TimeSpan.FromSeconds(Math.Max(1, settings.Worker.PollIntervalSeconds)), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PollOnceAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await _semaphore.WaitAsync(0, cancellationToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
if (string.IsNullOrWhiteSpace(settings.Database.Host) || string.IsNullOrWhiteSpace(settings.Printer.PrinterName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var jobs = await _repository.LoadPendingJobsAsync(5, cancellationToken);
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
await ProcessJobAsync(job, settings, cancellationToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessJobAsync(PrintJob job, AppSettings settings, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await _repository.MarkPrintingAsync(job.Id, cancellationToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var layout = _layoutStore.LoadByKey(job.LayoutKey);
|
||||
using var result = _renderer.Render(layout, job.PayloadJson);
|
||||
_printerService.Print(result.Bitmap, settings.Printer.PrinterName, layout.WidthMm, layout.HeightMm);
|
||||
await _repository.MarkPrintedAsync(job.Id, settings.Printer.PrinterName, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not print job {JobId}", job.Id);
|
||||
await _repository.MarkErrorAsync(job.Id, ex.Message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
try
|
||||
{
|
||||
_task?.Wait(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_semaphore.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user