Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfe141efc9 | |||
| e3c153fb0f | |||
| 26f056684c | |||
| aef360402c | |||
| 7e40d61b9c |
@@ -75,11 +75,8 @@ jobs:
|
||||
dotnet publish src/LabelPrintAgent/LabelPrintAgent.csproj \
|
||||
--configuration Release \
|
||||
--runtime win-x64 \
|
||||
--self-contained true \
|
||||
--self-contained false \
|
||||
--output publish/win-x64 \
|
||||
-p:PublishSingleFile=true \
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||||
-p:EnableCompressionInSingleFile=true \
|
||||
-p:Version="$version" \
|
||||
-p:InformationalVersion="$tag+$GITHUB_SHA"
|
||||
|
||||
|
||||
+54
-7
@@ -12,7 +12,7 @@ Alle Endpunkte erfordern einen Bearer Token (JWT oder API-Key):
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
`/jobs/next`, `/jobs/:id/image`, `/jobs/:id/printed` und `/jobs/:id/error` benötigen keine spezifische Permission, nur einen gültigen Token. `POST /jobs` und `POST /preview` erfordern `VIEW_SCANNER`.
|
||||
`/printers/register`, `/printers/:printerId/deactivate`, `/jobs/next`, `/jobs/:id/image`, `/jobs/:id/printed` und `/jobs/:id/error` benötigen keine spezifische Permission, nur einen gültigen Token. `POST /jobs` und `POST /preview` erfordern `VIEW_SCANNER`.
|
||||
|
||||
## 1. Job manuell anlegen (Frontend -> Backend)
|
||||
|
||||
@@ -67,7 +67,48 @@ Content-Type: image/png
|
||||
|
||||
Body: binäres PNG-Bild.
|
||||
|
||||
## 3. Nächsten Druckjob abrufen (Agent-Polling)
|
||||
## 3. Drucker aktivieren/deaktivieren (Benutzeraktion in den Agent-Einstellungen)
|
||||
|
||||
Der Agent ruft diesen Endpunkt nicht automatisch beim Start auf. Der Benutzer aktiviert lokale Windows-Drucker bewusst über die Agent-Einstellungen.
|
||||
|
||||
```http
|
||||
POST /api/label-print-agent/printers/register
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"printerId": "PC-BUERO_ZEBRA_GK420D",
|
||||
"agentId": "PC-BUERO",
|
||||
"name": "Zebra GK420 Büro",
|
||||
"windowsPrinterName": "Zebra GK420d",
|
||||
"dpi": 203,
|
||||
"defaultWidthMm": 101,
|
||||
"defaultHeightMm": 76,
|
||||
"active": true
|
||||
}
|
||||
```
|
||||
|
||||
### Antwort
|
||||
|
||||
```http
|
||||
200 OK
|
||||
```
|
||||
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
Zum Deaktivieren entfernt der Benutzer den Haken `Aktiv`; der Agent ruft standardmäßig folgenden Endpunkt auf:
|
||||
|
||||
```http
|
||||
POST /api/label-print-agent/printers/{printerId}/deactivate
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
Der Request Body entspricht dem Aktivieren, aber mit `"active": false`.
|
||||
|
||||
## 4. Nächsten Druckjob abrufen (Agent-Polling)
|
||||
|
||||
```http
|
||||
GET /api/label-print-agent/jobs/next?agentId={agentId}
|
||||
@@ -92,7 +133,10 @@ Content-Type: application/json
|
||||
|
||||
```json
|
||||
{
|
||||
"jobId": "42",
|
||||
"jobId": 42,
|
||||
"printerId": "PC-BUERO_ZEBRA_GK420D",
|
||||
"labelType": "artikel",
|
||||
"windowsPrinterName": "Zebra GK420d",
|
||||
"labelImageBase64": "iVBORw0KGgoAAAANSUhEUgAA...",
|
||||
"labelImageContentType": "image/png",
|
||||
"labelWidthMm": 57,
|
||||
@@ -103,13 +147,16 @@ Content-Type: application/json
|
||||
| Feld | Beschreibung |
|
||||
| --- | --- |
|
||||
| `jobId` | Job-ID für Rückmeldungen |
|
||||
| `printerId` | Backend-ID des Zieldruckers |
|
||||
| `labelType` | Etikettart, z. B. `artikel` oder `text` |
|
||||
| `windowsPrinterName` | Exakter lokaler Windows-Druckername, auf den der Agent drucken soll |
|
||||
| `labelImageBase64` | Base64-PNG; `null` wenn Rendering fehlgeschlagen |
|
||||
| `labelImageContentType` | Immer `image/png` |
|
||||
| `labelWidthMm` / `labelHeightMm` | Etikettenmaß in mm |
|
||||
|
||||
Das Backend setzt beim Ausliefern einen Lock (5-Minuten-TTL). Jobs mit abgelaufenem Lock werden erneut angeboten.
|
||||
|
||||
## 4. Etikettbild separat abrufen
|
||||
## 5. Etikettbild separat abrufen
|
||||
|
||||
Alternativ zum Base64-Feld in `jobs/next`.
|
||||
|
||||
@@ -132,7 +179,7 @@ Body: binäres PNG-Bild.
|
||||
|
||||
Job oder Bild nicht vorhanden.
|
||||
|
||||
## 5. Erfolgreichen Druck melden
|
||||
## 6. Erfolgreichen Druck melden
|
||||
|
||||
```http
|
||||
POST /api/label-print-agent/jobs/{jobId}/printed
|
||||
@@ -162,7 +209,7 @@ Alle Felder optional; Fallback jeweils `""` / `unknown`.
|
||||
|
||||
Das Backend setzt den Job auf `printed`, speichert Zeitstempel und ruft die konfigurierte `LabelPrintedUrl` des Templates auf (`POST`).
|
||||
|
||||
## 6. Druckfehler melden
|
||||
## 7. Druckfehler melden
|
||||
|
||||
```http
|
||||
POST /api/label-print-agent/jobs/{jobId}/error
|
||||
@@ -206,7 +253,7 @@ createJob()
|
||||
(PrintedUrl) (ReleaseUrl)
|
||||
```
|
||||
|
||||
## 7. Server-Sent Events – neue Druckaufträge (Push)
|
||||
## 8. Server-Sent Events – neue Druckaufträge (Push)
|
||||
|
||||
```http
|
||||
GET /api/label-print-agent/events
|
||||
|
||||
@@ -38,7 +38,10 @@ Wenn ein Etikett vorhanden ist:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobId": "12345",
|
||||
"jobId": 12345,
|
||||
"printerId": "PC-BUERO_DYMO_LABELWRITER_450",
|
||||
"labelType": "artikel",
|
||||
"windowsPrinterName": "DYMO LabelWriter 450",
|
||||
"labelImageBase64": "...",
|
||||
"labelImageContentType": "image/png",
|
||||
"labelWidthMm": 57,
|
||||
@@ -50,7 +53,10 @@ Alternativ darf das Backend statt `labelImageBase64` eine URL liefern:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobId": "12345",
|
||||
"jobId": 12345,
|
||||
"printerId": "PC-BUERO_DYMO_LABELWRITER_450",
|
||||
"labelType": "artikel",
|
||||
"windowsPrinterName": "DYMO LabelWriter 450",
|
||||
"labelImageUrl": "/api/label-print-agent/jobs/12345/image",
|
||||
"labelImageContentType": "image/png",
|
||||
"labelWidthMm": 57,
|
||||
@@ -101,6 +107,8 @@ Beispiel:
|
||||
"baseUrl": "https://paperlessmanager.local",
|
||||
"agentId": "PC-BUERO",
|
||||
"encryptedApiToken": "",
|
||||
"registerPrinterPath": "/api/label-print-agent/printers/register",
|
||||
"deactivatePrinterPath": "/api/label-print-agent/printers/{printerId}/deactivate",
|
||||
"nextJobPath": "/api/label-print-agent/jobs/next",
|
||||
"imagePath": "/api/label-print-agent/jobs/{jobId}/image",
|
||||
"reportSuccessPath": "/api/label-print-agent/jobs/{jobId}/printed",
|
||||
@@ -114,6 +122,26 @@ Beispiel:
|
||||
"labelHeightMm": 32,
|
||||
"dpi": 300
|
||||
},
|
||||
"printers": [
|
||||
{
|
||||
"printerId": "PC-BUERO_DYMO_LABELWRITER_450",
|
||||
"name": "DYMO LabelWriter Regal",
|
||||
"windowsPrinterName": "DYMO LabelWriter 450",
|
||||
"dpi": 300,
|
||||
"defaultWidthMm": 57,
|
||||
"defaultHeightMm": 32,
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"printerId": "PC-BUERO_ZEBRA_GK420D",
|
||||
"name": "Zebra GK420 Büro",
|
||||
"windowsPrinterName": "Zebra GK420d",
|
||||
"dpi": 203,
|
||||
"defaultWidthMm": 101,
|
||||
"defaultHeightMm": 76,
|
||||
"isActive": false
|
||||
}
|
||||
],
|
||||
"worker": {
|
||||
"enabled": true,
|
||||
"pollIntervalSeconds": 30
|
||||
@@ -123,6 +151,8 @@ Beispiel:
|
||||
|
||||
Der API-Token wird lokal mit Windows DPAPI verschlüsselt gespeichert.
|
||||
|
||||
Der Agent registriert Drucker nicht automatisch. Im Tab `Drucker` steht links die Druckerliste; rechts werden Aktiv-Status, Breite, Höhe und DPI pro Drucker gepflegt. Wird `Aktiv` gesetzt, registriert der Agent den Drucker im Backend. Wird der Haken entfernt, ruft der Agent den Deaktivierungs-Endpunkt auf. Das Feld `printer` bleibt als Kompatibilitätsfeld für ältere Einstellungen erhalten; neue Jobs werden anhand von `windowsPrinterName` aus der Backend-Antwort auf den passenden lokalen Windows-Drucker gedruckt.
|
||||
|
||||
## Dymo-Druck
|
||||
|
||||
Der Dymo LabelWriter muss in Windows als normaler Drucker eingerichtet sein. Das Backend liefert ein fertiges Bild für das Etikett, typischerweise PNG in `57 x 32 mm`.
|
||||
@@ -139,10 +169,22 @@ Wichtig:
|
||||
1. Anwendung starten.
|
||||
2. Tray-Symbol öffnen.
|
||||
3. Im Tab `Backend` BaseUrl, AgentId und optional API-Token eintragen.
|
||||
4. Im Tab `Drucker` den Dymo LabelWriter auswählen.
|
||||
4. Im Tab `Drucker` links einen Drucker auswählen und rechts `Aktiv`, Breite, Höhe und DPI setzen.
|
||||
5. Im Tab `Allgemein` Polling aktivieren und Intervall setzen.
|
||||
6. Mit `Jetzt prüfen` kann sofort ein Poll-Lauf ausgelöst werden; dabei werden alle aktuell verfügbaren Jobs verarbeitet.
|
||||
|
||||
## Updates
|
||||
|
||||
Im Tab `Updates` kann der Agent gegen die Gitea-Release-API nach neuen Versionen suchen. Standardmäßig wird das neueste Release dieses Repositorys abgefragt:
|
||||
|
||||
```text
|
||||
https://gitea.poettker-cloud.de/api/v1/repos/bjoernpoettker/LabelPrintAgent/releases/latest
|
||||
```
|
||||
|
||||
Für private Repositories kann ein Gitea-Access-Token hinterlegt werden. Der Token wird lokal verschlüsselt gespeichert.
|
||||
|
||||
Wenn ein neueres Release gefunden wird, sucht der Agent nach einem ZIP-Asset, bevorzugt mit dem Suffix `-win-x64.zip`. Das ZIP enthält den framework-dependent `win-x64`-Publish inklusive `.exe`, App-Dateien und zugehöriger NuGet-`.dll`-Dateien, aber ohne die normale .NET-Laufzeit. Beim Installieren lädt der Agent das ZIP herunter, entpackt es in ein temporäres Verzeichnis, startet ein lokales Update-Skript, beendet sich selbst, ersetzt die Dateien im Installationsordner und startet anschließend neu.
|
||||
|
||||
## Tray-Status
|
||||
|
||||
Das Tray-Icon zeigt den aktuellen Zustand:
|
||||
|
||||
@@ -2,6 +2,7 @@ using LabelPrintAgent.Backend;
|
||||
using LabelPrintAgent.Configuration;
|
||||
using LabelPrintAgent.Logging;
|
||||
using LabelPrintAgent.Printing;
|
||||
using LabelPrintAgent.Updates;
|
||||
using Serilog;
|
||||
|
||||
namespace LabelPrintAgent.App;
|
||||
@@ -23,10 +24,11 @@ internal static class Program
|
||||
|
||||
var printerService = new PrinterService();
|
||||
var backendClient = new BackendClient(settingsStore);
|
||||
var updateService = new UpdateService(settingsStore);
|
||||
using var backendWorker = new BackendPollingWorker(settingsStore, backendClient, printerService);
|
||||
backendWorker.Start();
|
||||
|
||||
Application.Run(new TrayApplicationContext(settingsStore, printerService, backendWorker));
|
||||
Application.Run(new TrayApplicationContext(settingsStore, printerService, backendClient, backendWorker, updateService));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using LabelPrintAgent.Backend;
|
||||
using LabelPrintAgent.Configuration;
|
||||
using LabelPrintAgent.Printing;
|
||||
using LabelPrintAgent.Updates;
|
||||
using Microsoft.Win32;
|
||||
using Serilog;
|
||||
|
||||
@@ -13,7 +14,17 @@ internal sealed class SettingsForm : Form
|
||||
|
||||
private readonly SettingsStore _settingsStore;
|
||||
private readonly PrinterService _printerService;
|
||||
private readonly BackendClient _backendClient;
|
||||
private readonly BackendPollingWorker _backendWorker;
|
||||
private readonly UpdateService _updateService;
|
||||
private readonly Dictionary<string, PrinterConfig> _printerConfigs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private UpdateCheckResult? _lastUpdateCheck;
|
||||
private bool _loadingPrinters;
|
||||
private bool _loadingPrinterConfig;
|
||||
private string? _selectedSharedPrinterName;
|
||||
private decimal _defaultLabelWidthMm = 57;
|
||||
private decimal _defaultLabelHeightMm = 32;
|
||||
private int _defaultPrinterDpi = 203;
|
||||
|
||||
private readonly CheckBox _autoStartCheckBox = new() { Text = "Autostart aktivieren", AutoSize = true };
|
||||
private readonly CheckBox _workerEnabled = new() { Text = "Backend automatisch abfragen", AutoSize = true };
|
||||
@@ -23,6 +34,8 @@ internal sealed class SettingsForm : Form
|
||||
private readonly TextBox _backendBaseUrl = new() { Width = 420 };
|
||||
private readonly TextBox _agentId = new() { Width = 260 };
|
||||
private readonly TextBox _apiToken = new() { Width = 420, UseSystemPasswordChar = true };
|
||||
private readonly TextBox _registerPrinterPath = new() { Width = 420 };
|
||||
private readonly TextBox _deactivatePrinterPath = new() { Width = 420 };
|
||||
private readonly TextBox _nextJobPath = new() { Width = 420 };
|
||||
private readonly TextBox _imagePath = new() { Width = 420 };
|
||||
private readonly TextBox _reportSuccessPath = new() { Width = 420 };
|
||||
@@ -30,9 +43,19 @@ internal sealed class SettingsForm : Form
|
||||
private readonly CheckBox _useServerSentEvents = new() { Text = "Server-Sent Events verwenden", AutoSize = true };
|
||||
private readonly TextBox _eventsPath = new() { Width = 420 };
|
||||
|
||||
private readonly ComboBox _printer = new() { Width = 360, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||
private readonly TextBox _releaseApiUrl = new() { Width = 540 };
|
||||
private readonly TextBox _releaseToken = new() { Width = 420, UseSystemPasswordChar = true };
|
||||
private readonly TextBox _currentVersion = new() { Width = 180, ReadOnly = true };
|
||||
private readonly TextBox _latestVersion = new() { Width = 180, ReadOnly = true };
|
||||
private readonly TextBox _updateAsset = new() { Width = 540, ReadOnly = true };
|
||||
private readonly Button _installUpdateButton;
|
||||
|
||||
private readonly ListBox _printerList = new() { Width = 320, Height = 285 };
|
||||
private readonly CheckBox _printerActive = new() { Text = "Aktiv", AutoSize = true };
|
||||
private readonly TextBox _windowsPrinterName = new() { Width = 360, ReadOnly = true };
|
||||
private readonly NumericUpDown _labelWidth = new() { Minimum = 1, Maximum = 500, DecimalPlaces = 1, Value = 57, Width = 100 };
|
||||
private readonly NumericUpDown _labelHeight = new() { Minimum = 1, Maximum = 500, DecimalPlaces = 1, Value = 32, Width = 100 };
|
||||
private readonly NumericUpDown _printerDpi = new() { Minimum = 50, Maximum = 2400, Value = 203, Width = 100 };
|
||||
|
||||
private readonly TextBox _status = new()
|
||||
{
|
||||
@@ -42,11 +65,20 @@ internal sealed class SettingsForm : Form
|
||||
Dock = DockStyle.Fill
|
||||
};
|
||||
|
||||
public SettingsForm(SettingsStore settingsStore, PrinterService printerService, BackendPollingWorker backendWorker)
|
||||
public SettingsForm(
|
||||
SettingsStore settingsStore,
|
||||
PrinterService printerService,
|
||||
BackendClient backendClient,
|
||||
BackendPollingWorker backendWorker,
|
||||
UpdateService updateService)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_printerService = printerService;
|
||||
_backendClient = backendClient;
|
||||
_backendWorker = backendWorker;
|
||||
_updateService = updateService;
|
||||
_installUpdateButton = ButtonAt("Update installieren", 340, 265, async (_, _) => await InstallUpdateFromUiAsync());
|
||||
_installUpdateButton.Enabled = false;
|
||||
|
||||
Text = "LabelPrintAgent Einstellungen";
|
||||
Width = 900;
|
||||
@@ -57,8 +89,14 @@ internal sealed class SettingsForm : Form
|
||||
tabs.TabPages.Add(CreateGeneralTab());
|
||||
tabs.TabPages.Add(CreateBackendTab());
|
||||
tabs.TabPages.Add(CreatePrinterTab());
|
||||
tabs.TabPages.Add(CreateUpdatesTab());
|
||||
tabs.TabPages.Add(CreateStatusTab());
|
||||
Controls.Add(tabs);
|
||||
_printerList.SelectedIndexChanged += (_, _) => SelectPrinter(GetPrinterName(_printerList.SelectedItem));
|
||||
_printerActive.CheckedChanged += async (_, _) => await SetSelectedPrinterActiveFromUiAsync();
|
||||
_labelWidth.ValueChanged += (_, _) => SaveSelectedPrinterConfig();
|
||||
_labelHeight.ValueChanged += (_, _) => SaveSelectedPrinterConfig();
|
||||
_printerDpi.ValueChanged += (_, _) => SaveSelectedPrinterConfig();
|
||||
|
||||
Load += (_, _) =>
|
||||
{
|
||||
@@ -84,28 +122,58 @@ internal sealed class SettingsForm : Form
|
||||
panel.Controls.Add(Row(20, Label("BaseUrl", 160), _backendBaseUrl));
|
||||
panel.Controls.Add(Row(60, Label("AgentId", 160), _agentId));
|
||||
panel.Controls.Add(Row(100, Label("API Token", 160), _apiToken));
|
||||
panel.Controls.Add(Row(140, Label("NextJobPath", 160), _nextJobPath));
|
||||
panel.Controls.Add(Row(180, Label("ImagePath", 160), _imagePath));
|
||||
panel.Controls.Add(Row(220, Label("ReportSuccessPath", 160), _reportSuccessPath));
|
||||
panel.Controls.Add(Row(260, Label("ReportErrorPath", 160), _reportErrorPath));
|
||||
panel.Controls.Add(Row(300, Label("SSE", 160), _useServerSentEvents));
|
||||
panel.Controls.Add(Row(340, Label("EventsPath", 160), _eventsPath));
|
||||
panel.Controls.Add(ButtonAt("Speichern", 180, 395, (_, _) => SaveBackend(showMessage: true)));
|
||||
panel.Controls.Add(ButtonAt("Jetzt prüfen", 300, 395, async (_, _) => await PollOnceFromUiAsync()));
|
||||
panel.Controls.Add(Row(140, Label("RegisterPrinterPath", 160), _registerPrinterPath));
|
||||
panel.Controls.Add(Row(180, Label("DeactivatePrinterPath", 160), _deactivatePrinterPath));
|
||||
panel.Controls.Add(Row(220, Label("NextJobPath", 160), _nextJobPath));
|
||||
panel.Controls.Add(Row(260, Label("ImagePath", 160), _imagePath));
|
||||
panel.Controls.Add(Row(300, Label("ReportSuccessPath", 160), _reportSuccessPath));
|
||||
panel.Controls.Add(Row(340, Label("ReportErrorPath", 160), _reportErrorPath));
|
||||
panel.Controls.Add(Row(380, Label("SSE", 160), _useServerSentEvents));
|
||||
panel.Controls.Add(Row(420, Label("EventsPath", 160), _eventsPath));
|
||||
panel.Controls.Add(ButtonAt("Speichern", 180, 475, (_, _) => SaveBackend(showMessage: true)));
|
||||
panel.Controls.Add(ButtonAt("Jetzt prüfen", 300, 475, async (_, _) => await PollOnceFromUiAsync()));
|
||||
return new TabPage("Backend") { Controls = { panel } };
|
||||
}
|
||||
|
||||
private TabPage CreatePrinterTab()
|
||||
{
|
||||
var panel = CreatePaddedPanel();
|
||||
panel.Controls.Add(Row(20, Label("Drucker", 120), _printer));
|
||||
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("Speichern", 300, 150, (_, _) => SavePrinter(showMessage: true)));
|
||||
var listLabel = Label("Drucker", 120);
|
||||
listLabel.Left = 20;
|
||||
listLabel.Top = 20;
|
||||
_printerList.Left = 20;
|
||||
_printerList.Top = 55;
|
||||
panel.Controls.Add(listLabel);
|
||||
panel.Controls.Add(_printerList);
|
||||
|
||||
var detailLeft = 380;
|
||||
_printerActive.Left = detailLeft;
|
||||
_printerActive.Top = 55;
|
||||
panel.Controls.Add(_printerActive);
|
||||
panel.Controls.Add(RowAt(detailLeft, 95, Label("Windows-Name", 120), _windowsPrinterName));
|
||||
panel.Controls.Add(RowAt(detailLeft, 135, Label("Breite", 120), _labelWidth, Label("mm", 40)));
|
||||
panel.Controls.Add(RowAt(detailLeft, 175, Label("Höhe", 120), _labelHeight, Label("mm", 40)));
|
||||
panel.Controls.Add(RowAt(detailLeft, 215, Label("DPI", 120), _printerDpi));
|
||||
|
||||
panel.Controls.Add(ButtonAt("Drucker neu laden", 20, 365, (_, _) => LoadPrinters()));
|
||||
panel.Controls.Add(ButtonAt("Speichern", detailLeft, 365, (_, _) => SavePrinter(showMessage: true)));
|
||||
return new TabPage("Drucker") { Controls = { panel } };
|
||||
}
|
||||
|
||||
private TabPage CreateUpdatesTab()
|
||||
{
|
||||
var panel = CreatePaddedPanel();
|
||||
panel.Controls.Add(Row(20, Label("Aktuelle Version", 160), _currentVersion));
|
||||
panel.Controls.Add(Row(60, Label("Neueste Version", 160), _latestVersion));
|
||||
panel.Controls.Add(Row(100, Label("Release API", 160), _releaseApiUrl));
|
||||
panel.Controls.Add(Row(140, Label("Access Token", 160), _releaseToken));
|
||||
panel.Controls.Add(Row(180, Label("Installationsdatei", 160), _updateAsset));
|
||||
panel.Controls.Add(ButtonAt("Speichern", 180, 265, (_, _) => SaveUpdates(showMessage: true)));
|
||||
panel.Controls.Add(ButtonAt("Nach Updates suchen", 180, 315, async (_, _) => await CheckForUpdateFromUiAsync()));
|
||||
panel.Controls.Add(_installUpdateButton);
|
||||
return new TabPage("Updates") { Controls = { panel } };
|
||||
}
|
||||
|
||||
private TabPage CreateStatusTab()
|
||||
{
|
||||
var page = new TabPage("Status");
|
||||
@@ -124,6 +192,8 @@ internal sealed class SettingsForm : Form
|
||||
_backendBaseUrl.Text = settings.Backend.BaseUrl;
|
||||
_agentId.Text = settings.Backend.AgentId;
|
||||
_apiToken.Text = _settingsStore.DecryptBackendApiToken(settings);
|
||||
_registerPrinterPath.Text = settings.Backend.RegisterPrinterPath;
|
||||
_deactivatePrinterPath.Text = settings.Backend.DeactivatePrinterPath;
|
||||
_nextJobPath.Text = settings.Backend.NextJobPath;
|
||||
_imagePath.Text = settings.Backend.ImagePath;
|
||||
_reportSuccessPath.Text = settings.Backend.ReportSuccessPath;
|
||||
@@ -131,26 +201,62 @@ internal sealed class SettingsForm : Form
|
||||
_useServerSentEvents.Checked = settings.Backend.UseServerSentEvents;
|
||||
_eventsPath.Text = settings.Backend.EventsPath;
|
||||
|
||||
_labelWidth.Value = settings.Printer.LabelWidthMm;
|
||||
_labelHeight.Value = settings.Printer.LabelHeightMm;
|
||||
_releaseApiUrl.Text = settings.Updates.ReleaseApiUrl;
|
||||
_releaseToken.Text = _settingsStore.DecryptUpdateAccessToken(settings);
|
||||
_currentVersion.Text = _updateService.CurrentVersion.ToString();
|
||||
_latestVersion.Text = string.Empty;
|
||||
_updateAsset.Text = string.Empty;
|
||||
_installUpdateButton.Enabled = false;
|
||||
_lastUpdateCheck = null;
|
||||
|
||||
_defaultLabelWidthMm = settings.Printer.LabelWidthMm;
|
||||
_defaultLabelHeightMm = settings.Printer.LabelHeightMm;
|
||||
_defaultPrinterDpi = settings.Printer.Dpi > 0 ? settings.Printer.Dpi : 203;
|
||||
SetPrinterEditorEnabled(false);
|
||||
}
|
||||
|
||||
private void LoadPrinters()
|
||||
{
|
||||
var selected = _settingsStore.Load().Printer.PrinterName;
|
||||
_printer.Items.Clear();
|
||||
var settings = _settingsStore.Load();
|
||||
var selected = GetPrimaryPrinter(settings)?.WindowsPrinterName ?? settings.Printer.PrinterName;
|
||||
_loadingPrinters = true;
|
||||
_selectedSharedPrinterName = null;
|
||||
_printerConfigs.Clear();
|
||||
foreach (var printer in settings.Printers.Where(printer => !string.IsNullOrWhiteSpace(printer.WindowsPrinterName)))
|
||||
{
|
||||
_printerConfigs[printer.WindowsPrinterName] = ClonePrinterConfig(printer);
|
||||
}
|
||||
|
||||
_printerList.Items.Clear();
|
||||
var printers = _printerService.GetInstalledPrinters();
|
||||
foreach (var printer in printers)
|
||||
{
|
||||
_printer.Items.Add(printer);
|
||||
EnsurePrinterConfig(printer.Name);
|
||||
_printerList.Items.Add(printer);
|
||||
}
|
||||
|
||||
var configuredPrinter = printers.FirstOrDefault(printer => string.Equals(printer.Name, selected, StringComparison.OrdinalIgnoreCase));
|
||||
_printer.SelectedItem = configuredPrinter ?? printers.FirstOrDefault(printer => printer.IsDefault);
|
||||
if (_printer.SelectedItem is null && _printer.Items.Count > 0)
|
||||
foreach (var printerName in _printerConfigs.Keys
|
||||
.Where(name => printers.All(printer => !string.Equals(printer.Name, name, StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(name => name))
|
||||
{
|
||||
_printer.SelectedIndex = 0;
|
||||
_printerList.Items.Add(printerName);
|
||||
}
|
||||
|
||||
_loadingPrinters = false;
|
||||
var selectedIndex = FindPrinterIndex(selected);
|
||||
if (selectedIndex < 0 && _printerList.Items.Count > 0)
|
||||
{
|
||||
selectedIndex = 0;
|
||||
}
|
||||
|
||||
if (selectedIndex >= 0)
|
||||
{
|
||||
_printerList.SelectedIndex = selectedIndex;
|
||||
SelectPrinter(GetPrinterName(_printerList.SelectedItem));
|
||||
return;
|
||||
}
|
||||
|
||||
SetPrinterEditorEnabled(false);
|
||||
}
|
||||
|
||||
private void SaveGeneral()
|
||||
@@ -171,6 +277,8 @@ internal sealed class SettingsForm : Form
|
||||
var normalizedToken = SettingsStore.NormalizeBackendApiToken(_apiToken.Text);
|
||||
settings.Backend.EncryptedApiToken = _settingsStore.EncryptPassword(normalizedToken);
|
||||
_apiToken.Text = normalizedToken;
|
||||
settings.Backend.RegisterPrinterPath = _registerPrinterPath.Text.Trim();
|
||||
settings.Backend.DeactivatePrinterPath = _deactivatePrinterPath.Text.Trim();
|
||||
settings.Backend.NextJobPath = _nextJobPath.Text.Trim();
|
||||
settings.Backend.ImagePath = _imagePath.Text.Trim();
|
||||
settings.Backend.ReportSuccessPath = _reportSuccessPath.Text.Trim();
|
||||
@@ -188,9 +296,22 @@ internal sealed class SettingsForm : Form
|
||||
private void SavePrinter(bool showMessage)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
settings.Printer.PrinterName = GetSelectedPrinterName() ?? string.Empty;
|
||||
var selectedPrinterName = GetSelectedPrinterName() ?? string.Empty;
|
||||
SaveSelectedPrinterConfig();
|
||||
settings.Printer.PrinterName = selectedPrinterName;
|
||||
settings.Printer.LabelWidthMm = _labelWidth.Value;
|
||||
settings.Printer.LabelHeightMm = _labelHeight.Value;
|
||||
settings.Printer.Dpi = (int)_printerDpi.Value;
|
||||
settings.Printers = _printerConfigs.Keys
|
||||
.OrderBy(printerName => printerName)
|
||||
.Select(printerName => BuildPrinterConfig(settings, printerName, fallbackToEditor: false))
|
||||
.ToList();
|
||||
|
||||
if (settings.Printers.Count == 0 && !string.IsNullOrWhiteSpace(selectedPrinterName))
|
||||
{
|
||||
settings.Printers.Add(BuildPrinterConfig(settings, selectedPrinterName, fallbackToEditor: true));
|
||||
}
|
||||
|
||||
_settingsStore.Save(settings);
|
||||
AppendStatus("Druckereinstellungen gespeichert.");
|
||||
if (showMessage)
|
||||
@@ -199,6 +320,19 @@ internal sealed class SettingsForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveUpdates(bool showMessage)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
settings.Updates.ReleaseApiUrl = _releaseApiUrl.Text.Trim();
|
||||
settings.Updates.EncryptedAccessToken = _settingsStore.EncryptPassword(_releaseToken.Text.Trim());
|
||||
_settingsStore.Save(settings);
|
||||
AppendStatus("Update-Einstellungen gespeichert.");
|
||||
if (showMessage)
|
||||
{
|
||||
MessageBox.Show("Update-Einstellungen gespeichert.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PollOnceFromUiAsync()
|
||||
{
|
||||
try
|
||||
@@ -217,9 +351,229 @@ internal sealed class SettingsForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckForUpdateFromUiAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
SaveUpdates(showMessage: false);
|
||||
_installUpdateButton.Enabled = false;
|
||||
_lastUpdateCheck = null;
|
||||
_latestVersion.Text = string.Empty;
|
||||
_updateAsset.Text = string.Empty;
|
||||
|
||||
AppendStatus("Suche nach neuen Releases...");
|
||||
var result = await _updateService.CheckForUpdateAsync();
|
||||
_lastUpdateCheck = result;
|
||||
_currentVersion.Text = result.CurrentVersion.ToString();
|
||||
_latestVersion.Text = result.Release.TagName;
|
||||
_updateAsset.Text = result.InstallAsset?.Name ?? "Kein ZIP-Asset gefunden";
|
||||
|
||||
if (!result.IsUpdateAvailable)
|
||||
{
|
||||
AppendStatus($"Kein Update verfügbar. Installiert: {result.CurrentVersion}, Release: {result.Release.TagName}.");
|
||||
MessageBox.Show("Es ist kein Update verfügbar.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.InstallAsset is null)
|
||||
{
|
||||
AppendStatus($"Update {result.Release.TagName} gefunden, aber kein ZIP-Asset zum Installieren.");
|
||||
MessageBox.Show("Update gefunden, aber das Release enthält kein ZIP-Asset.", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_installUpdateButton.Enabled = true;
|
||||
AppendStatus($"Update {result.Release.TagName} gefunden: {result.InstallAsset.Name}");
|
||||
MessageBox.Show($"Update {result.Release.TagName} ist verfügbar.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Update check failed");
|
||||
AppendStatus($"Update-Prüfung fehlgeschlagen: {ex.Message}");
|
||||
MessageBox.Show($"Update-Prüfung fehlgeschlagen: {ex.Message}", Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InstallUpdateFromUiAsync()
|
||||
{
|
||||
if (_lastUpdateCheck is null || _lastUpdateCheck.InstallAsset is null)
|
||||
{
|
||||
MessageBox.Show("Bitte zuerst nach Updates suchen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmation = MessageBox.Show(
|
||||
$"Update {_lastUpdateCheck.Release.TagName} installieren? Die Anwendung wird danach neu gestartet.",
|
||||
Text,
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (confirmation != DialogResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
AppendStatus($"Update {_lastUpdateCheck.Release.TagName} wird heruntergeladen...");
|
||||
await _updateService.InstallUpdateAsync(_lastUpdateCheck);
|
||||
AppendStatus("Update-Skript gestartet. Anwendung wird beendet.");
|
||||
Application.Exit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Update installation failed");
|
||||
AppendStatus($"Update-Installation fehlgeschlagen: {ex.Message}");
|
||||
MessageBox.Show($"Update-Installation fehlgeschlagen: {ex.Message}", Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetSelectedPrinterName()
|
||||
{
|
||||
return _printer.SelectedItem switch
|
||||
return GetPrinterName(_printerList.SelectedItem);
|
||||
}
|
||||
|
||||
private void SelectPrinter(string? printerName)
|
||||
{
|
||||
if (_loadingPrinters)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(_selectedSharedPrinterName, printerName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveSelectedPrinterConfig();
|
||||
if (string.IsNullOrWhiteSpace(printerName) || !_printerConfigs.TryGetValue(printerName, out var config))
|
||||
{
|
||||
_selectedSharedPrinterName = null;
|
||||
SetPrinterEditorEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedSharedPrinterName = printerName;
|
||||
SetPrinterEditorEnabled(true);
|
||||
_loadingPrinterConfig = true;
|
||||
try
|
||||
{
|
||||
_windowsPrinterName.Text = config.WindowsPrinterName;
|
||||
_printerActive.Checked = config.IsActive;
|
||||
_labelWidth.Value = Math.Clamp(config.DefaultWidthMm, (int)_labelWidth.Minimum, (int)_labelWidth.Maximum);
|
||||
_labelHeight.Value = Math.Clamp(config.DefaultHeightMm, (int)_labelHeight.Minimum, (int)_labelHeight.Maximum);
|
||||
_printerDpi.Value = Math.Clamp(config.Dpi, (int)_printerDpi.Minimum, (int)_printerDpi.Maximum);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingPrinterConfig = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveSelectedPrinterConfig()
|
||||
{
|
||||
if (_loadingPrinters || _loadingPrinterConfig || string.IsNullOrWhiteSpace(_selectedSharedPrinterName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var config = EnsurePrinterConfig(_selectedSharedPrinterName);
|
||||
config.DefaultWidthMm = Math.Max(1, (int)Math.Round(_labelWidth.Value));
|
||||
config.DefaultHeightMm = Math.Max(1, (int)Math.Round(_labelHeight.Value));
|
||||
config.Dpi = (int)_printerDpi.Value;
|
||||
config.IsActive = _printerActive.Checked;
|
||||
}
|
||||
|
||||
private async Task SetSelectedPrinterActiveFromUiAsync()
|
||||
{
|
||||
if (_loadingPrinters || _loadingPrinterConfig || string.IsNullOrWhiteSpace(_selectedSharedPrinterName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SaveBackend(showMessage: false);
|
||||
SaveSelectedPrinterConfig();
|
||||
var config = BuildPrinterConfig(_settingsStore.Load(), _selectedSharedPrinterName, fallbackToEditor: false);
|
||||
config.IsActive = _printerActive.Checked;
|
||||
SavePrinter(showMessage: false);
|
||||
await _backendClient.SetPrinterActiveAsync(config, config.IsActive);
|
||||
AppendStatus($"Drucker {config.WindowsPrinterName} {(config.IsActive ? "aktiviert" : "deaktiviert")}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not update printer active state");
|
||||
AppendStatus($"Aktiv-Status konnte nicht synchronisiert werden: {ex.Message}");
|
||||
MessageBox.Show($"Aktiv-Status konnte nicht synchronisiert werden: {ex.Message}", Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private PrinterConfig EnsurePrinterConfig(string printerName)
|
||||
{
|
||||
if (_printerConfigs.TryGetValue(printerName, out var config))
|
||||
{
|
||||
return config;
|
||||
}
|
||||
|
||||
config = new PrinterConfig
|
||||
{
|
||||
PrinterId = SettingsStore.BuildPrinterId(_agentId.Text.Trim(), printerName),
|
||||
Name = printerName,
|
||||
WindowsPrinterName = printerName,
|
||||
Dpi = _defaultPrinterDpi,
|
||||
DefaultWidthMm = Math.Max(1, (int)Math.Round(_defaultLabelWidthMm)),
|
||||
DefaultHeightMm = Math.Max(1, (int)Math.Round(_defaultLabelHeightMm)),
|
||||
IsActive = false
|
||||
};
|
||||
_printerConfigs[printerName] = config;
|
||||
return config;
|
||||
}
|
||||
|
||||
private PrinterConfig BuildPrinterConfig(AppSettings settings, string printerName, bool fallbackToEditor)
|
||||
{
|
||||
var existingPrinter = _printerConfigs.TryGetValue(printerName, out var config)
|
||||
? config
|
||||
: settings.Printers.FirstOrDefault(printer => string.Equals(printer.WindowsPrinterName, printerName, StringComparison.OrdinalIgnoreCase));
|
||||
var width = fallbackToEditor ? (int)Math.Round(_labelWidth.Value) : existingPrinter?.DefaultWidthMm ?? (int)Math.Round(settings.Printer.LabelWidthMm);
|
||||
var height = fallbackToEditor ? (int)Math.Round(_labelHeight.Value) : existingPrinter?.DefaultHeightMm ?? (int)Math.Round(settings.Printer.LabelHeightMm);
|
||||
var dpi = fallbackToEditor ? (int)_printerDpi.Value : existingPrinter?.Dpi ?? settings.Printer.Dpi;
|
||||
return new PrinterConfig
|
||||
{
|
||||
PrinterId = string.IsNullOrWhiteSpace(existingPrinter?.PrinterId)
|
||||
? SettingsStore.BuildPrinterId(settings.Backend.AgentId, printerName)
|
||||
: existingPrinter.PrinterId,
|
||||
Name = string.IsNullOrWhiteSpace(existingPrinter?.Name) ? printerName : existingPrinter.Name,
|
||||
WindowsPrinterName = printerName,
|
||||
Dpi = Math.Max(1, dpi),
|
||||
DefaultWidthMm = Math.Max(1, width),
|
||||
DefaultHeightMm = Math.Max(1, height),
|
||||
IsActive = existingPrinter?.IsActive ?? false
|
||||
};
|
||||
}
|
||||
|
||||
private static PrinterConfig? GetPrimaryPrinter(AppSettings settings)
|
||||
{
|
||||
return settings.Printers.FirstOrDefault(printer => printer.IsActive && !string.IsNullOrWhiteSpace(printer.WindowsPrinterName))
|
||||
?? settings.Printers.FirstOrDefault(printer => !string.IsNullOrWhiteSpace(printer.WindowsPrinterName));
|
||||
}
|
||||
|
||||
private int FindPrinterIndex(string printerName)
|
||||
{
|
||||
for (var index = 0; index < _printerList.Items.Count; index++)
|
||||
{
|
||||
if (string.Equals(GetPrinterName(_printerList.Items[index]), printerName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static string? GetPrinterName(object? item)
|
||||
{
|
||||
return item switch
|
||||
{
|
||||
PrinterInfo printerInfo => printerInfo.Name,
|
||||
string printerName => printerName,
|
||||
@@ -227,6 +581,36 @@ internal sealed class SettingsForm : Form
|
||||
};
|
||||
}
|
||||
|
||||
private static PrinterConfig ClonePrinterConfig(PrinterConfig printer)
|
||||
{
|
||||
return new PrinterConfig
|
||||
{
|
||||
PrinterId = printer.PrinterId,
|
||||
Name = printer.Name,
|
||||
WindowsPrinterName = printer.WindowsPrinterName,
|
||||
Dpi = printer.Dpi,
|
||||
DefaultWidthMm = printer.DefaultWidthMm,
|
||||
DefaultHeightMm = printer.DefaultHeightMm,
|
||||
IsActive = printer.IsActive
|
||||
};
|
||||
}
|
||||
|
||||
private void SetPrinterEditorEnabled(bool enabled)
|
||||
{
|
||||
_labelWidth.Enabled = enabled;
|
||||
_labelHeight.Enabled = enabled;
|
||||
_printerDpi.Enabled = enabled;
|
||||
_printerActive.Enabled = enabled;
|
||||
_windowsPrinterName.Enabled = enabled;
|
||||
if (!enabled)
|
||||
{
|
||||
_loadingPrinterConfig = true;
|
||||
_windowsPrinterName.Text = string.Empty;
|
||||
_printerActive.Checked = false;
|
||||
_loadingPrinterConfig = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendStatus(string message)
|
||||
{
|
||||
_status.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}{Environment.NewLine}");
|
||||
@@ -256,10 +640,15 @@ internal sealed class SettingsForm : Form
|
||||
private static Label Label(string text, int width) => new() { Text = text, Width = width, TextAlign = ContentAlignment.MiddleLeft };
|
||||
|
||||
private static FlowLayoutPanel Row(int top, params Control[] controls)
|
||||
{
|
||||
return RowAt(20, top, controls);
|
||||
}
|
||||
|
||||
private static FlowLayoutPanel RowAt(int left, int top, params Control[] controls)
|
||||
{
|
||||
var row = new FlowLayoutPanel
|
||||
{
|
||||
Left = 20,
|
||||
Left = left,
|
||||
Top = top,
|
||||
Width = 760,
|
||||
Height = 32,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using LabelPrintAgent.Backend;
|
||||
using LabelPrintAgent.Configuration;
|
||||
using LabelPrintAgent.Printing;
|
||||
using LabelPrintAgent.Updates;
|
||||
using Serilog;
|
||||
|
||||
namespace LabelPrintAgent.App;
|
||||
@@ -10,7 +11,9 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
private readonly NotifyIcon _notifyIcon;
|
||||
private readonly SettingsStore _settingsStore;
|
||||
private readonly PrinterService _printerService;
|
||||
private readonly BackendClient _backendClient;
|
||||
private readonly BackendPollingWorker _backendWorker;
|
||||
private readonly UpdateService _updateService;
|
||||
private readonly Icon _healthyIcon;
|
||||
private readonly Icon _unhealthyIcon;
|
||||
private readonly System.Windows.Forms.Timer _statusTimer;
|
||||
@@ -19,11 +22,15 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
public TrayApplicationContext(
|
||||
SettingsStore settingsStore,
|
||||
PrinterService printerService,
|
||||
BackendPollingWorker backendWorker)
|
||||
BackendClient backendClient,
|
||||
BackendPollingWorker backendWorker,
|
||||
UpdateService updateService)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_printerService = printerService;
|
||||
_backendClient = backendClient;
|
||||
_backendWorker = backendWorker;
|
||||
_updateService = updateService;
|
||||
_healthyIcon = TrayIconFactory.CreatePrinterIcon(Color.FromArgb(36, 160, 72));
|
||||
_unhealthyIcon = TrayIconFactory.CreatePrinterIcon(Color.FromArgb(210, 48, 48));
|
||||
|
||||
@@ -58,7 +65,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
{
|
||||
if (_settingsForm is null || _settingsForm.IsDisposed)
|
||||
{
|
||||
_settingsForm = new SettingsForm(_settingsStore, _printerService, _backendWorker);
|
||||
_settingsForm = new SettingsForm(_settingsStore, _printerService, _backendClient, _backendWorker, _updateService);
|
||||
}
|
||||
|
||||
_settingsForm.Show();
|
||||
@@ -108,12 +115,15 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
return (false, "Backend-URL fehlt.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.Printer.PrinterName))
|
||||
var configuredPrinters = settings.Printers
|
||||
.Where(printer => printer.IsActive && !string.IsNullOrWhiteSpace(printer.WindowsPrinterName))
|
||||
.ToList();
|
||||
if (configuredPrinters.Count == 0)
|
||||
{
|
||||
return (false, "Drucker fehlt.");
|
||||
}
|
||||
|
||||
if (!_printerService.IsPrinterAvailable(settings.Printer.PrinterName))
|
||||
if (!configuredPrinters.Any(printer => _printerService.IsPrinterAvailable(printer.WindowsPrinterName)))
|
||||
{
|
||||
return (false, "Drucker nicht verfügbar.");
|
||||
}
|
||||
|
||||
@@ -35,6 +35,21 @@ public sealed class BackendClient
|
||||
return await response.Content.ReadFromJsonAsync<BackendLabelJob>(cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public async Task RegisterPrintersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
foreach (var printer in settings.Printers.Where(printer => printer.IsActive).Where(IsRegisterablePrinter))
|
||||
{
|
||||
await SetPrinterActiveAsync(settings, printer, active: true, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetPrinterActiveAsync(PrinterConfig printer, bool active, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
await SetPrinterActiveAsync(settings, printer, active, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Bitmap> GetLabelImageAsync(BackendLabelJob job, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(job.LabelImageBase64))
|
||||
@@ -46,7 +61,7 @@ public sealed class BackendClient
|
||||
if (string.IsNullOrWhiteSpace(job.LabelImageUrl))
|
||||
{
|
||||
var settingsForPath = _settingsStore.Load();
|
||||
job.LabelImageUrl = settingsForPath.Backend.ImagePath.Replace("{jobId}", Uri.EscapeDataString(job.JobId), StringComparison.OrdinalIgnoreCase);
|
||||
job.LabelImageUrl = settingsForPath.Backend.ImagePath.Replace("{jobId}", Uri.EscapeDataString(job.JobId.ToString()), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var settings = _settingsStore.Load();
|
||||
@@ -57,7 +72,7 @@ public sealed class BackendClient
|
||||
return CreateBitmap(bytesFromUrl);
|
||||
}
|
||||
|
||||
public Task ReportPrintedAsync(string jobId, string printerName, CancellationToken cancellationToken = default)
|
||||
public Task ReportPrintedAsync(int jobId, string printerName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
var request = new BackendReportRequest
|
||||
@@ -68,7 +83,7 @@ public sealed class BackendClient
|
||||
return PostReportAsync(settings, settings.Backend.ReportSuccessPath, jobId, request, cancellationToken);
|
||||
}
|
||||
|
||||
public Task ReportErrorAsync(string jobId, string printerName, string errorMessage, CancellationToken cancellationToken = default)
|
||||
public Task ReportErrorAsync(int jobId, string printerName, string errorMessage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
var request = new BackendReportRequest
|
||||
@@ -80,15 +95,49 @@ public sealed class BackendClient
|
||||
return PostReportAsync(settings, settings.Backend.ReportErrorPath, jobId, request, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task PostReportAsync(AppSettings settings, string pathTemplate, string jobId, BackendReportRequest body, CancellationToken cancellationToken)
|
||||
private async Task PostReportAsync(AppSettings settings, string pathTemplate, int jobId, BackendReportRequest body, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = pathTemplate.Replace("{jobId}", Uri.EscapeDataString(jobId), StringComparison.OrdinalIgnoreCase);
|
||||
var path = pathTemplate.Replace("{jobId}", Uri.EscapeDataString(jobId.ToString()), StringComparison.OrdinalIgnoreCase);
|
||||
using var request = CreateRequest(HttpMethod.Post, MakeAbsoluteUrl(settings.Backend.BaseUrl, path), settings);
|
||||
request.Content = JsonContent.Create(body);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
await EnsureSuccessWithDiagnosticsAsync(response, request, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task SetPrinterActiveAsync(AppSettings settings, PrinterConfig printer, bool active, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsRegisterablePrinter(printer))
|
||||
{
|
||||
throw new InvalidOperationException("Drucker kann ohne PrinterId und WindowsPrinterName nicht synchronisiert werden.");
|
||||
}
|
||||
|
||||
var body = new
|
||||
{
|
||||
printerId = printer.PrinterId,
|
||||
agentId = settings.Backend.AgentId,
|
||||
name = printer.Name,
|
||||
windowsPrinterName = printer.WindowsPrinterName,
|
||||
dpi = printer.Dpi,
|
||||
defaultWidthMm = printer.DefaultWidthMm,
|
||||
defaultHeightMm = printer.DefaultHeightMm,
|
||||
active
|
||||
};
|
||||
|
||||
var path = active
|
||||
? settings.Backend.RegisterPrinterPath
|
||||
: settings.Backend.DeactivatePrinterPath.Replace("{printerId}", Uri.EscapeDataString(printer.PrinterId), StringComparison.OrdinalIgnoreCase);
|
||||
using var request = CreateRequest(HttpMethod.Post, MakeAbsoluteUrl(settings.Backend.BaseUrl, path), settings);
|
||||
request.Content = JsonContent.Create(body);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
await EnsureSuccessWithDiagnosticsAsync(response, request, cancellationToken);
|
||||
Log.Information(
|
||||
"{Action} printer {PrinterId} ({WindowsPrinterName}) for agent {AgentId}",
|
||||
active ? "Activated" : "Deactivated",
|
||||
printer.PrinterId,
|
||||
printer.WindowsPrinterName,
|
||||
settings.Backend.AgentId);
|
||||
}
|
||||
|
||||
public async Task WatchServerSentEventsAsync(Func<CancellationToken, Task> onLabelJobAvailable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
@@ -217,6 +266,12 @@ public sealed class BackendClient
|
||||
return new Bitmap(stream);
|
||||
}
|
||||
|
||||
private static bool IsRegisterablePrinter(PrinterConfig printer)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(printer.PrinterId)
|
||||
&& !string.IsNullOrWhiteSpace(printer.WindowsPrinterName);
|
||||
}
|
||||
|
||||
private static bool IsLabelJobAvailableEvent(string data)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(data))
|
||||
|
||||
@@ -2,7 +2,10 @@ namespace LabelPrintAgent.Backend;
|
||||
|
||||
public sealed class BackendLabelJob
|
||||
{
|
||||
public string JobId { get; set; } = string.Empty;
|
||||
public int JobId { get; set; }
|
||||
public string PrinterId { get; set; } = string.Empty;
|
||||
public string LabelType { get; set; } = string.Empty;
|
||||
public string WindowsPrinterName { get; set; } = string.Empty;
|
||||
public string? LabelImageBase64 { get; set; }
|
||||
public string? LabelImageUrl { get; set; }
|
||||
public string LabelImageContentType { get; set; } = "image/png";
|
||||
|
||||
@@ -54,11 +54,24 @@ public sealed class BackendPollingWorker : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var printerName = settings.Printer.PrinterName;
|
||||
if (string.IsNullOrWhiteSpace(printerName) || !_printerService.IsPrinterAvailable(printerName))
|
||||
var configuredPrinters = settings.Printers
|
||||
.Where(printer => printer.IsActive && !string.IsNullOrWhiteSpace(printer.WindowsPrinterName))
|
||||
.ToList();
|
||||
if (configuredPrinters.Count == 0)
|
||||
{
|
||||
Log.Warning("No available printer configured for backend polling");
|
||||
SetStatus(false, "Kein verfügbarer Drucker konfiguriert.");
|
||||
SetStatus(false, "Kein Drucker konfiguriert.");
|
||||
return;
|
||||
}
|
||||
|
||||
var unavailablePrinters = configuredPrinters
|
||||
.Where(printer => !_printerService.IsPrinterAvailable(printer.WindowsPrinterName))
|
||||
.Select(printer => printer.WindowsPrinterName)
|
||||
.ToList();
|
||||
if (unavailablePrinters.Count == configuredPrinters.Count)
|
||||
{
|
||||
Log.Warning("No configured printer is available for backend polling: {Printers}", string.Join(", ", unavailablePrinters));
|
||||
SetStatus(false, "Kein konfigurierter Drucker verfügbar.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,7 +89,7 @@ public sealed class BackendPollingWorker : IDisposable
|
||||
}
|
||||
|
||||
processedJobs++;
|
||||
await ProcessJobAsync(job, printerName, cancellationToken);
|
||||
await ProcessJobAsync(job, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
@@ -90,10 +103,21 @@ public sealed class BackendPollingWorker : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessJobAsync(BackendLabelJob job, string printerName, CancellationToken cancellationToken)
|
||||
private async Task ProcessJobAsync(BackendLabelJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
var printerName = job.WindowsPrinterName;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(printerName))
|
||||
{
|
||||
throw new InvalidOperationException("Der Druckjob enthält keinen windowsPrinterName.");
|
||||
}
|
||||
|
||||
if (!_printerService.IsPrinterAvailable(printerName))
|
||||
{
|
||||
throw new InvalidOperationException($"Drucker '{printerName}' ist nicht verfügbar.");
|
||||
}
|
||||
|
||||
SetStatus(true, $"Job {job.JobId} wird gedruckt.");
|
||||
using var bitmap = await _backendClient.GetLabelImageAsync(job, cancellationToken);
|
||||
var result = await _printerService.PrintAsync(bitmap, printerName, job.LabelWidthMm, job.LabelHeightMm, cancellationToken);
|
||||
@@ -110,7 +134,7 @@ public sealed class BackendPollingWorker : IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not process backend label job {JobId}", job.JobId);
|
||||
await _backendClient.ReportErrorAsync(job.JobId, printerName, ex.Message, cancellationToken);
|
||||
await _backendClient.ReportErrorAsync(job.JobId, printerName ?? string.Empty, ex.Message, cancellationToken);
|
||||
SetStatus(false, $"Job {job.JobId} fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ namespace LabelPrintAgent.Configuration;
|
||||
public sealed class AppSettings
|
||||
{
|
||||
public BackendSettings Backend { get; set; } = new();
|
||||
public UpdateSettings Updates { get; set; } = new();
|
||||
public PrinterSettings Printer { get; set; } = new();
|
||||
public List<PrinterConfig> Printers { get; set; } = [];
|
||||
public WorkerSettings Worker { get; set; } = new();
|
||||
public PathSettings Paths { get; set; } = PathSettings.CreateDefault();
|
||||
|
||||
@@ -19,6 +21,8 @@ public sealed class BackendSettings
|
||||
public string BaseUrl { get; set; } = "https://paperlessmanager.local";
|
||||
public string AgentId { get; set; } = Environment.MachineName;
|
||||
public string EncryptedApiToken { get; set; } = string.Empty;
|
||||
public string RegisterPrinterPath { get; set; } = "/api/label-print-agent/printers/register";
|
||||
public string DeactivatePrinterPath { get; set; } = "/api/label-print-agent/printers/{printerId}/deactivate";
|
||||
public string NextJobPath { get; set; } = "/api/label-print-agent/jobs/next";
|
||||
public string ImagePath { get; set; } = "/api/label-print-agent/jobs/{jobId}/image";
|
||||
public string ReportSuccessPath { get; set; } = "/api/label-print-agent/jobs/{jobId}/printed";
|
||||
@@ -27,6 +31,13 @@ public sealed class BackendSettings
|
||||
public string EventsPath { get; set; } = "/api/label-print-agent/events";
|
||||
}
|
||||
|
||||
public sealed class UpdateSettings
|
||||
{
|
||||
public string ReleaseApiUrl { get; set; } =
|
||||
"https://gitea.poettker-cloud.de/api/v1/repos/bjoernpoettker/LabelPrintAgent/releases/latest";
|
||||
public string EncryptedAccessToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class PrinterSettings
|
||||
{
|
||||
public string PrinterName { get; set; } = string.Empty;
|
||||
@@ -35,6 +46,17 @@ public sealed class PrinterSettings
|
||||
public int Dpi { get; set; } = 300;
|
||||
}
|
||||
|
||||
public sealed class PrinterConfig
|
||||
{
|
||||
public string PrinterId { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string WindowsPrinterName { get; set; } = string.Empty;
|
||||
public int Dpi { get; set; } = 203;
|
||||
public int DefaultWidthMm { get; set; } = 57;
|
||||
public int DefaultHeightMm { get; set; } = 32;
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class WorkerSettings
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
@@ -38,6 +38,12 @@ public sealed class SettingsStore
|
||||
var json = File.ReadAllText(SettingsFilePath);
|
||||
var settings = JsonSerializer.Deserialize<AppSettings>(json, JsonOptions) ?? new AppSettings();
|
||||
settings.Paths ??= PathSettings.CreateDefault();
|
||||
settings.Backend ??= new BackendSettings();
|
||||
settings.Updates ??= new UpdateSettings();
|
||||
settings.Printer ??= new PrinterSettings();
|
||||
settings.Printers ??= [];
|
||||
settings.Worker ??= new WorkerSettings();
|
||||
MigrateLegacyPrinter(settings);
|
||||
return settings;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -58,6 +64,8 @@ public sealed class SettingsStore
|
||||
|
||||
public string DecryptBackendApiToken(AppSettings settings) => _protectedStringService.Unprotect(settings.Backend.EncryptedApiToken);
|
||||
|
||||
public string DecryptUpdateAccessToken(AppSettings settings) => _protectedStringService.Unprotect(settings.Updates.EncryptedAccessToken);
|
||||
|
||||
public static string NormalizeBackendApiToken(string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
@@ -78,4 +86,34 @@ public sealed class SettingsStore
|
||||
|
||||
return normalized.Trim().Trim('"', '\'').Trim();
|
||||
}
|
||||
|
||||
private static void MigrateLegacyPrinter(AppSettings settings)
|
||||
{
|
||||
if (settings.Printers.Count > 0 || string.IsNullOrWhiteSpace(settings.Printer.PrinterName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
settings.Printers.Add(new PrinterConfig
|
||||
{
|
||||
PrinterId = BuildPrinterId(settings.Backend.AgentId, settings.Printer.PrinterName),
|
||||
Name = settings.Printer.PrinterName,
|
||||
WindowsPrinterName = settings.Printer.PrinterName,
|
||||
Dpi = settings.Printer.Dpi,
|
||||
DefaultWidthMm = Math.Max(1, (int)Math.Round(settings.Printer.LabelWidthMm)),
|
||||
DefaultHeightMm = Math.Max(1, (int)Math.Round(settings.Printer.LabelHeightMm)),
|
||||
IsActive = true
|
||||
});
|
||||
}
|
||||
|
||||
public static string BuildPrinterId(string agentId, string windowsPrinterName)
|
||||
{
|
||||
var effectiveAgentId = string.IsNullOrWhiteSpace(agentId) ? Environment.MachineName : agentId.Trim();
|
||||
var printerPart = new string((windowsPrinterName ?? string.Empty)
|
||||
.Trim()
|
||||
.Select(character => char.IsLetterOrDigit(character) ? char.ToUpperInvariant(character) : '_')
|
||||
.ToArray());
|
||||
printerPart = string.Join("_", printerPart.Split('_', StringSplitOptions.RemoveEmptyEntries));
|
||||
return $"{effectiveAgentId}_{(string.IsNullOrWhiteSpace(printerPart) ? "PRINTER" : printerPart)}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace LabelPrintAgent.Updates;
|
||||
|
||||
internal sealed record ReleaseAsset(string Name, string DownloadUrl, long Size);
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace LabelPrintAgent.Updates;
|
||||
|
||||
internal sealed record ReleaseInfo(
|
||||
string TagName,
|
||||
string Name,
|
||||
string HtmlUrl,
|
||||
IReadOnlyList<ReleaseAsset> Assets)
|
||||
{
|
||||
public ReleaseAsset? FindWindowsZipAsset()
|
||||
{
|
||||
return Assets.FirstOrDefault(asset =>
|
||||
asset.Name.EndsWith("-win-x64.zip", StringComparison.OrdinalIgnoreCase))
|
||||
?? Assets.FirstOrDefault(asset =>
|
||||
asset.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace LabelPrintAgent.Updates;
|
||||
|
||||
internal sealed record UpdateCheckResult(
|
||||
Version CurrentVersion,
|
||||
ReleaseInfo Release,
|
||||
bool IsUpdateAvailable)
|
||||
{
|
||||
public ReleaseAsset? InstallAsset => Release.FindWindowsZipAsset();
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using LabelPrintAgent.Configuration;
|
||||
|
||||
namespace LabelPrintAgent.Updates;
|
||||
|
||||
internal sealed class UpdateService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private readonly SettingsStore _settingsStore;
|
||||
|
||||
public UpdateService(SettingsStore settingsStore)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
public Version CurrentVersion => GetCurrentVersion();
|
||||
|
||||
public async Task<UpdateCheckResult> CheckForUpdateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
if (string.IsNullOrWhiteSpace(settings.Updates.ReleaseApiUrl))
|
||||
{
|
||||
throw new InvalidOperationException("Die Release-API-URL fehlt.");
|
||||
}
|
||||
|
||||
using var client = CreateHttpClient(settings);
|
||||
using var response = await client.GetAsync(settings.Updates.ReleaseApiUrl, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
var release = await JsonSerializer.DeserializeAsync<GiteaReleaseResponse>(stream, JsonOptions, cancellationToken)
|
||||
?? throw new InvalidOperationException("Die Release-Antwort konnte nicht gelesen werden.");
|
||||
|
||||
var latest = MapRelease(release);
|
||||
var latestVersion = ParseVersion(latest.TagName);
|
||||
var currentVersion = CurrentVersion;
|
||||
return new UpdateCheckResult(currentVersion, latest, latestVersion > currentVersion);
|
||||
}
|
||||
|
||||
public async Task InstallUpdateAsync(UpdateCheckResult checkResult, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var asset = checkResult.InstallAsset
|
||||
?? throw new InvalidOperationException("Das Release enthält kein installierbares ZIP-Asset.");
|
||||
|
||||
var installFolder = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
var executablePath = Application.ExecutablePath;
|
||||
var updateRoot = Path.Combine(Path.GetTempPath(), "LabelPrintAgent", "updates", checkResult.Release.TagName);
|
||||
var downloadFile = Path.Combine(updateRoot, asset.Name);
|
||||
var extractFolder = Path.Combine(updateRoot, "extracted");
|
||||
var updaterScript = Path.Combine(updateRoot, "apply-update.cmd");
|
||||
|
||||
Directory.CreateDirectory(updateRoot);
|
||||
if (Directory.Exists(extractFolder))
|
||||
{
|
||||
Directory.Delete(extractFolder, recursive: true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(extractFolder);
|
||||
|
||||
var settings = _settingsStore.Load();
|
||||
using var client = CreateHttpClient(settings);
|
||||
await using (var source = await client.GetStreamAsync(asset.DownloadUrl, cancellationToken))
|
||||
await using (var target = File.Create(downloadFile))
|
||||
{
|
||||
await source.CopyToAsync(target, cancellationToken);
|
||||
}
|
||||
|
||||
ZipFile.ExtractToDirectory(downloadFile, extractFolder, overwriteFiles: true);
|
||||
WriteUpdaterScript(updaterScript, extractFolder, installFolder, executablePath, Environment.ProcessId);
|
||||
StartUpdater(updaterScript);
|
||||
}
|
||||
|
||||
private HttpClient CreateHttpClient(AppSettings settings)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("LabelPrintAgent");
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
var token = _settingsStore.DecryptUpdateAccessToken(settings).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", token);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private static ReleaseInfo MapRelease(GiteaReleaseResponse release)
|
||||
{
|
||||
var assets = release.Assets
|
||||
.Where(asset => !string.IsNullOrWhiteSpace(asset.Name) && !string.IsNullOrWhiteSpace(asset.BrowserDownloadUrl))
|
||||
.Select(asset => new ReleaseAsset(asset.Name, asset.BrowserDownloadUrl, asset.Size))
|
||||
.ToList();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(release.TagName))
|
||||
{
|
||||
throw new InvalidOperationException("Das Release enthält keinen Tag.");
|
||||
}
|
||||
|
||||
return new ReleaseInfo(
|
||||
release.TagName,
|
||||
string.IsNullOrWhiteSpace(release.Name) ? release.TagName : release.Name,
|
||||
release.HtmlUrl,
|
||||
assets);
|
||||
}
|
||||
|
||||
private static Version GetCurrentVersion()
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var informational = assembly
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(informational))
|
||||
{
|
||||
return ParseVersion(informational);
|
||||
}
|
||||
|
||||
return assembly.GetName().Version ?? new Version(0, 0, 0);
|
||||
}
|
||||
|
||||
private static Version ParseVersion(string value)
|
||||
{
|
||||
var cleaned = value.Trim();
|
||||
if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cleaned = cleaned[1..];
|
||||
}
|
||||
|
||||
var metadataIndex = cleaned.IndexOf('+', StringComparison.Ordinal);
|
||||
if (metadataIndex >= 0)
|
||||
{
|
||||
cleaned = cleaned[..metadataIndex];
|
||||
}
|
||||
|
||||
var prereleaseIndex = cleaned.IndexOf('-', StringComparison.Ordinal);
|
||||
if (prereleaseIndex >= 0)
|
||||
{
|
||||
cleaned = cleaned[..prereleaseIndex];
|
||||
}
|
||||
|
||||
return Version.TryParse(cleaned, out var version)
|
||||
? version
|
||||
: new Version(0, 0, 0);
|
||||
}
|
||||
|
||||
private static void WriteUpdaterScript(
|
||||
string scriptPath,
|
||||
string sourceFolder,
|
||||
string targetFolder,
|
||||
string executablePath,
|
||||
int processId)
|
||||
{
|
||||
var script = $"""
|
||||
@echo off
|
||||
setlocal
|
||||
set "SOURCE={sourceFolder}"
|
||||
set "TARGET={targetFolder}"
|
||||
set "EXE={executablePath}"
|
||||
set "PID={processId}"
|
||||
|
||||
:wait
|
||||
tasklist /FI "PID eq %PID%" 2>NUL | find "%PID%" >NUL
|
||||
if not errorlevel 1 (
|
||||
timeout /t 1 /nobreak >NUL
|
||||
goto wait
|
||||
)
|
||||
|
||||
xcopy "%SOURCE%\*" "%TARGET%\" /E /I /Y >NUL
|
||||
start "" "%EXE%"
|
||||
del "%~f0"
|
||||
""";
|
||||
|
||||
File.WriteAllText(scriptPath, script);
|
||||
}
|
||||
|
||||
private static void StartUpdater(string scriptPath)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = scriptPath,
|
||||
UseShellExecute = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class GiteaReleaseResponse
|
||||
{
|
||||
[JsonPropertyName("tag_name")]
|
||||
public string TagName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("html_url")]
|
||||
public string HtmlUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("assets")]
|
||||
public List<GiteaReleaseAssetResponse> Assets { get; set; } = [];
|
||||
}
|
||||
|
||||
private sealed class GiteaReleaseAssetResponse
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("browser_download_url")]
|
||||
public string BrowserDownloadUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public long Size { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user