feat(20-windows-client): aplikacja C# WinForms do zdalnego druku etykiet
- System tray app z NotifyIcon + ContextMenuStrip - Polling API orderPRO (GET /api/print/jobs/pending) - Pobieranie etykiet PDF i druk przez PdfiumViewer - Formularz ustawień: URL API, klucz, drukarka, interwał - Okno logów z historią (ciemny motyw, Consolas) - Self-contained .NET 8 publish (win-x64) - Milestone v0.7 Zdalne drukowanie etykiet — COMPLETE Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,11 +37,14 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i n
|
||||
- [x] Szablony wiadomości e-mail (CRUD + Quill.js + system zmiennych + załączniki) — Phase 14
|
||||
- [x] Wysyłka e-mail z zamówień (resolwer zmiennych, załączniki, log) — Phase 15
|
||||
- [x] Zadania automatyczne — reguły zdarzenie/warunki/akcje (CRUD + watcher/executor) — Phase 16
|
||||
- [x] Ostrzeżenie i potwierdzenie przy duplikacie paragonu — Phase 17
|
||||
- [x] Print Queue Backend: REST API + API key auth + CRUD kluczy — Phase 18
|
||||
- [x] UI Integration: przycisk Drukuj, bulk print, kolejka wydruku — Phase 19
|
||||
- [x] Windows Client: C# WinForms tray app, polling API, druk etykiet PDF — Phase 20
|
||||
|
||||
### Active (In Progress)
|
||||
|
||||
- [x] Ostrzeżenie i potwierdzenie przy duplikacie paragonu — Phase 17
|
||||
- [x] Print Queue Backend: REST API + API key auth + CRUD kluczy — Phase 18
|
||||
(brak — gotowe do v0.8)
|
||||
|
||||
### Planned (Next)
|
||||
|
||||
|
||||
@@ -6,18 +6,25 @@ orderPRO to narzędzie do wielokanałowego zarządzania sprzedażą. Projekt prz
|
||||
|
||||
## Current Milestone
|
||||
|
||||
### v0.7 Zdalne drukowanie etykiet — In progress
|
||||
|
||||
System zdalnego drukowania etykiet przesyłek na drukarce termicznej (Xprinter XP-420B). Aplikacja Windows w system tray odpytuje API orderPRO, pobiera zlecenia i drukuje etykiety A6.
|
||||
|
||||
| Phase | Name | Plans | Status |
|
||||
|-------|------|-------|--------|
|
||||
| 18 | Print Queue Backend | 1/1 | Complete ✓ |
|
||||
| 19 | UI Integration | 1/1 | Complete ✓ |
|
||||
| 20 | Windows Client (C# WinForms) | - | Not started |
|
||||
None — ready for v0.8 planning.
|
||||
|
||||
## Completed Milestones
|
||||
|
||||
<details>
|
||||
<summary>v0.7 Zdalne drukowanie etykiet — 2026-03-22 (3 phases, 3 plans)</summary>
|
||||
|
||||
System zdalnego drukowania etykiet przesyłek na drukarce termicznej. Aplikacja Windows w system tray odpytuje API orderPRO, pobiera zlecenia i drukuje etykiety A6.
|
||||
|
||||
| Phase | Name | Plans | Completed |
|
||||
|-------|------|-------|-----------|
|
||||
| 18 | Print Queue Backend | 1/1 | 2026-03-22 |
|
||||
| 19 | UI Integration | 1/1 | 2026-03-22 |
|
||||
| 20 | Windows Client (C# WinForms) | 1/1 | 2026-03-22 |
|
||||
|
||||
Archive: `.paul/phases/18-print-queue-backend/`, `.paul/phases/19-ui-integration/`, `.paul/phases/20-windows-client/`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v0.6 Poprawki UX — 2026-03-22 (1 phase, 1 plan)</summary>
|
||||
|
||||
@@ -106,4 +113,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-03-12*
|
||||
*Last updated: 2026-03-22 — v0.7 Phase 19 complete, Phase 20 next*
|
||||
*Last updated: 2026-03-22 — v0.7 milestone complete*
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
See: .paul/PROJECT.md (updated 2026-03-12)
|
||||
|
||||
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
|
||||
**Current focus:** v0.7 Zdalne drukowanie etykiet — Phase 19 Complete, transition required
|
||||
**Current focus:** v0.7 Zdalne drukowanie etykiet — MILESTONE COMPLETE ✓
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v0.7 Zdalne drukowanie etykiet
|
||||
Phase: [2] of [3] (UI Integration) — COMPLETE ✓
|
||||
Plan: 19-01 — loop closed
|
||||
Status: Phase 19 complete — transition required
|
||||
Last activity: 2026-03-22 — UNIFY complete, sonar-scanner ✓
|
||||
Milestone: v0.7 Zdalne drukowanie etykiet — COMPLETE ✓
|
||||
Phase: [3] of [3] (Windows Client) — COMPLETE ✓
|
||||
Plan: 20-01 — loop closed
|
||||
Status: Milestone v0.7 complete
|
||||
Last activity: 2026-03-22 — UNIFY complete, milestone v0.7 done
|
||||
|
||||
Progress:
|
||||
- v0.1 Initial Release: [██████████] 100% ✓
|
||||
@@ -22,16 +22,17 @@ Progress:
|
||||
- v0.4 Moduł E-mail: [██████████] 100% ✓
|
||||
- v0.5 Moduł Automatyzacji: [██████████] 100% ✓
|
||||
- v0.6 Poprawki UX: [██████████] 100% ✓
|
||||
- v0.7 Zdalne drukowanie etykiet: [██████░░░░] 67%
|
||||
- v0.7 Zdalne drukowanie etykiet: [██████████] 100% ✓
|
||||
- Phase 18: [██████████] 100% ✓ (1/1 plans)
|
||||
- Phase 19: [██████████] 100% ✓ (1/1 plans)
|
||||
- Phase 20: [██████████] 100% ✓ (1/1 plans)
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ✓ ✓ [Loop complete — phase transition required]
|
||||
✓ ✓ ✓ [Milestone v0.7 complete]
|
||||
```
|
||||
|
||||
## Accumulated Context
|
||||
@@ -65,6 +66,11 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
||||
| 2026-03-17 | Email history jako wpisy w order_activity_log (nie osobna sekcja) | Faza 15 | Spójność z istniejącym UX — jeden timeline zamiast fragmentacji |
|
||||
| 2026-03-17 | VariableResolver wydzielony z EmailTemplateController | Faza 15 | Reuse logiki zmiennych; resolwer niezależny od kontrolera szablonów |
|
||||
|
||||
### Skill Audit (Faza 20, Plan 01)
|
||||
| Oczekiwany | Wywołany | Uwagi |
|
||||
|------------|---------|-------|
|
||||
| sonar-scanner | n/a | Projekt C# — poza zakresem skanera PHP |
|
||||
|
||||
### Skill Audit (Faza 19, Plan 01)
|
||||
| Oczekiwany | Wywołany | Uwagi |
|
||||
|------------|---------|-------|
|
||||
@@ -189,9 +195,9 @@ Brak.
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-22
|
||||
Stopped at: Phase 19 complete — transition required
|
||||
Next action: Phase transition (ROADMAP update, git commit, route to next phase)
|
||||
Resume file: .paul/phases/19-ui-integration/19-01-SUMMARY.md
|
||||
Stopped at: Milestone v0.7 complete
|
||||
Next action: /paul:discuss-milestone lub /paul:milestone dla v0.8
|
||||
Resume file: .paul/phases/20-windows-client/20-01-SUMMARY.md
|
||||
Resume context:
|
||||
- v0.1: COMPLETE ✓ (6 phases, 15 plans)
|
||||
- v0.2: COMPLETE ✓ (1 phase, 5 plans)
|
||||
@@ -199,7 +205,7 @@ Resume context:
|
||||
- v0.4: COMPLETE ✓ (3 phases, 4 plans) — Moduł E-mail
|
||||
- v0.5: COMPLETE ✓ (1 phase, 2 plans) — Moduł Automatyzacji
|
||||
- v0.6: COMPLETE ✓ (1 phase, 1 plan) — Poprawki UX
|
||||
- v0.7: IN PROGRESS — Phase 18 ✓, Phase 19 ✓, Phase 20 next (2/3 phases complete)
|
||||
- v0.7: COMPLETE ✓ (3 phases, 3 plans) — Zdalne drukowanie etykiet
|
||||
|
||||
---
|
||||
*STATE.md — Updated after every significant action*
|
||||
|
||||
324
.paul/phases/20-windows-client/20-01-PLAN.md
Normal file
324
.paul/phases/20-windows-client/20-01-PLAN.md
Normal file
@@ -0,0 +1,324 @@
|
||||
---
|
||||
phase: 20-windows-client
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["18-01", "19-01"]
|
||||
files_modified:
|
||||
- clients/windows/OrderPROPrint/OrderPROPrint.sln
|
||||
- clients/windows/OrderPROPrint/OrderPROPrint.csproj
|
||||
- clients/windows/OrderPROPrint/Program.cs
|
||||
- clients/windows/OrderPROPrint/TrayApplicationContext.cs
|
||||
- clients/windows/OrderPROPrint/Services/PrintApiClient.cs
|
||||
- clients/windows/OrderPROPrint/Services/PrintService.cs
|
||||
- clients/windows/OrderPROPrint/Services/PollingService.cs
|
||||
- clients/windows/OrderPROPrint/Forms/SettingsForm.cs
|
||||
- clients/windows/OrderPROPrint/Forms/SettingsForm.Designer.cs
|
||||
- clients/windows/OrderPROPrint/Models/PrintJob.cs
|
||||
- clients/windows/OrderPROPrint/Models/AppSettings.cs
|
||||
- clients/windows/OrderPROPrint/Properties/Resources.resx
|
||||
- clients/windows/OrderPROPrint/app.config
|
||||
autonomous: false
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Stworzyć aplikację C# WinForms działającą w system tray, która odpytuje API orderPRO o zlecenia wydruku, pobiera etykiety i drukuje je na drukarce termicznej Xprinter XP-420B (format A6).
|
||||
|
||||
## Purpose
|
||||
Użytkownik może zlecić wydruk etykiety z przeglądarki (faza 19) i etykieta automatycznie wydrukuje się na drukarce podłączonej do jego komputera — bez ręcznego pobierania pliku i drukowania.
|
||||
|
||||
## Output
|
||||
- Aplikacja WinForms (.NET 8) w `clients/windows/OrderPROPrint/`
|
||||
- System tray icon z menu kontekstowym
|
||||
- Polling API co N sekund (konfigurowalne)
|
||||
- Automatyczny druk etykiet A6 na wybranej drukarce
|
||||
- Formularz ustawień (URL API, klucz API, drukarka, interwał)
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/18-print-queue-backend/18-01-SUMMARY.md
|
||||
- API endpoints (API key auth via X-Api-Key header, SHA-256):
|
||||
- GET /api/print/jobs/pending — lista zleceń do wydruku (JSON array)
|
||||
- GET /api/print/jobs/{id}/download — pobieranie pliku etykiety (binary)
|
||||
- POST /api/print/jobs/{id}/complete — oznaczenie jako wydrukowane
|
||||
- Auth: header X-Api-Key z raw key (serwer hashuje SHA-256 i porównuje)
|
||||
|
||||
@.paul/phases/19-ui-integration/19-01-SUMMARY.md
|
||||
- UI tworzy print jobs przez POST /api/print/jobs i /api/print/jobs/bulk
|
||||
- Status "pending" = gotowe do pobrania przez Windows Client
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Printing/PrintApiController.php — API endpoints consumed by this client
|
||||
@src/Modules/Printing/ApiKeyMiddleware.php — auth pattern (X-Api-Key header)
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
|
||||
|
||||
## Skill Invocation Checklist
|
||||
- [ ] sonar-scanner loaded (run before UNIFY) — uwaga: skanuje tylko PHP, C# poza zakresem
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Aplikacja uruchamia się w system tray
|
||||
```gherkin
|
||||
Given aplikacja OrderPROPrint jest uruchomiona
|
||||
When użytkownik patrzy na system tray (obszar powiadomień)
|
||||
Then widzi ikonę aplikacji OrderPROPrint
|
||||
And po kliknięciu prawym przyciskiem widzi menu: "Ustawienia", "Wstrzymaj/Wznów", "O programie", "Zamknij"
|
||||
And dwuklik otwiera formularz ustawień
|
||||
```
|
||||
|
||||
## AC-2: Konfiguracja połączenia z API
|
||||
```gherkin
|
||||
Given użytkownik otworzył formularz ustawień
|
||||
When wpisuje URL API (np. https://orderpro.projectpro.pl), klucz API i wybiera drukarkę
|
||||
Then ustawienia zapisują się w pliku konfiguracyjnym (app.config lub JSON)
|
||||
And po ponownym uruchomieniu ustawienia są zapamiętane
|
||||
And przycisk "Testuj połączenie" weryfikuje poprawność URL + klucza
|
||||
```
|
||||
|
||||
## AC-3: Polling i pobieranie zleceń
|
||||
```gherkin
|
||||
Given aplikacja ma poprawną konfigurację API
|
||||
When polling timer odpytuje GET /api/print/jobs/pending
|
||||
Then pobiera listę zleceń ze statusem "pending"
|
||||
And dla każdego zlecenia pobiera etykietę GET /api/print/jobs/{id}/download
|
||||
And po wydruku oznacza POST /api/print/jobs/{id}/complete
|
||||
And ikona tray pokazuje liczbę przetworzonych zleceń (tooltip lub balloon)
|
||||
```
|
||||
|
||||
## AC-4: Drukowanie etykiety A6 na drukarce termicznej
|
||||
```gherkin
|
||||
Given pobrano plik etykiety (PDF)
|
||||
When aplikacja wysyła do wybranej drukarki
|
||||
Then etykieta drukuje się w formacie A6 (105×148mm)
|
||||
And orientacja i marginesy są poprawne dla drukarki termicznej
|
||||
And w razie błędu druku zlecenie nie jest oznaczane jako complete
|
||||
```
|
||||
|
||||
## AC-5: Obsługa błędów i odporność
|
||||
```gherkin
|
||||
Given aplikacja jest uruchomiona
|
||||
When API jest niedostępne lub klucz nieprawidłowy
|
||||
Then ikona tray zmienia się na stan "error" (np. czerwona)
|
||||
And tooltip pokazuje ostatni błąd
|
||||
And polling kontynuuje próby (nie crash)
|
||||
And po przywróceniu połączenia wraca do normalnego działania
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Projekt C# WinForms + system tray + ustawienia</name>
|
||||
<files>
|
||||
clients/windows/OrderPROPrint/OrderPROPrint.sln,
|
||||
clients/windows/OrderPROPrint/OrderPROPrint.csproj,
|
||||
clients/windows/OrderPROPrint/Program.cs,
|
||||
clients/windows/OrderPROPrint/TrayApplicationContext.cs,
|
||||
clients/windows/OrderPROPrint/Forms/SettingsForm.cs,
|
||||
clients/windows/OrderPROPrint/Forms/SettingsForm.Designer.cs,
|
||||
clients/windows/OrderPROPrint/Models/AppSettings.cs,
|
||||
clients/windows/OrderPROPrint/Properties/Resources.resx
|
||||
</files>
|
||||
<action>
|
||||
1. Utwórz projekt .NET 8 WinForms w `clients/windows/OrderPROPrint/`:
|
||||
- `dotnet new winforms -n OrderPROPrint`
|
||||
- Target: net8.0-windows
|
||||
- NuGet: System.Text.Json (wbudowane), żadnych zewnętrznych zależności
|
||||
|
||||
2. Program.cs:
|
||||
- Application.Run(new TrayApplicationContext())
|
||||
- Nie pokazuj głównego okna — tylko tray
|
||||
|
||||
3. TrayApplicationContext (dziedziczy ApplicationContext):
|
||||
- NotifyIcon z ikoną (embedded resource lub SystemIcons.Application)
|
||||
- ContextMenuStrip z pozycjami:
|
||||
- "Ustawienia" → otwiera SettingsForm
|
||||
- "Wstrzymaj" / "Wznów" → toggle polling
|
||||
- separator
|
||||
- "O programie" → MessageBox z wersją
|
||||
- "Zamknij" → Application.Exit()
|
||||
- DoubleClick na ikonę → otwiera SettingsForm
|
||||
- Tooltip: "OrderPRO Print — Oczekiwanie" (aktualizowany przez PollingService)
|
||||
|
||||
4. AppSettings (model):
|
||||
- ApiUrl (string), ApiKey (string), PrinterName (string), PollIntervalSeconds (int, default 10)
|
||||
- Zapis/odczyt do JSON: %APPDATA%/OrderPROPrint/settings.json
|
||||
- Metoda Load() i Save() — statyczne
|
||||
|
||||
5. SettingsForm (WinForms designer):
|
||||
- Pola: txtApiUrl, txtApiKey, cmbPrinter, nudPollInterval
|
||||
- cmbPrinter: wypełniony z System.Drawing.Printing.PrinterSettings.InstalledPrinters
|
||||
- Przycisk "Testuj połączenie" — GET /api/print/jobs/pending z podanym URL+key
|
||||
- Sukces: zielony label "Połączono ✓"
|
||||
- Błąd: czerwony label z komunikatem
|
||||
- Przycisk "Zapisz" — AppSettings.Save() + zamknij formularz
|
||||
- Przycisk "Anuluj" — zamknij bez zapisu
|
||||
|
||||
Avoid: Nie używaj WPF ani MAUI — czysty WinForms dla prostoty
|
||||
Avoid: Nie dodawaj auto-start na tym etapie (to future enhancement)
|
||||
</action>
|
||||
<verify>
|
||||
- `dotnet build` przechodzi bez błędów
|
||||
- Aplikacja uruchamia się i pojawia się ikona w system tray
|
||||
- Menu kontekstowe działa
|
||||
- Formularz ustawień otwiera się i zamyka
|
||||
- Lista drukarek się ładuje
|
||||
- Ustawienia zapisują się do %APPDATA%/OrderPROPrint/settings.json
|
||||
</verify>
|
||||
<done>AC-1 satisfied (system tray), AC-2 satisfied (konfiguracja)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: API client + polling + drukowanie</name>
|
||||
<files>
|
||||
clients/windows/OrderPROPrint/Services/PrintApiClient.cs,
|
||||
clients/windows/OrderPROPrint/Services/PrintService.cs,
|
||||
clients/windows/OrderPROPrint/Services/PollingService.cs,
|
||||
clients/windows/OrderPROPrint/Models/PrintJob.cs,
|
||||
clients/windows/OrderPROPrint/TrayApplicationContext.cs
|
||||
</files>
|
||||
<action>
|
||||
1. PrintJob (model):
|
||||
- Id (int), OrderId (int), PackageId (int), LabelPath (string), Status (string)
|
||||
- Deserializacja z JSON response GET /api/print/jobs/pending
|
||||
|
||||
2. PrintApiClient:
|
||||
- Constructor: (string apiUrl, string apiKey)
|
||||
- HttpClient z default header X-Api-Key
|
||||
- async Task<List<PrintJob>> GetPendingJobsAsync()
|
||||
- GET {apiUrl}/api/print/jobs/pending
|
||||
- Deserializuj JSON response → List<PrintJob>
|
||||
- async Task<byte[]> DownloadLabelAsync(int jobId)
|
||||
- GET {apiUrl}/api/print/jobs/{jobId}/download
|
||||
- Return raw bytes (PDF)
|
||||
- async Task<bool> MarkCompleteAsync(int jobId)
|
||||
- POST {apiUrl}/api/print/jobs/{jobId}/complete
|
||||
- Return true jeśli 200
|
||||
- async Task<bool> TestConnectionAsync()
|
||||
- GET {apiUrl}/api/print/jobs/pending (sprawdza czy nie 401)
|
||||
- Obsługa błędów: HttpRequestException, TaskCanceledException (timeout)
|
||||
|
||||
3. PrintService:
|
||||
- static void PrintPdf(byte[] pdfBytes, string printerName)
|
||||
- Zapisz PDF do temp file → drukuj przez Process.Start z parametrami drukarki
|
||||
- Alternatywa: użyj System.Drawing.Printing.PrintDocument z renderowaniem PDF
|
||||
- Preferowane podejście: zapisz temp PDF, użyj SumatraPDF CLI (portable) do silent print:
|
||||
`SumatraPDF.exe -print-to "PrinterName" -silent label.pdf`
|
||||
- Jeśli SumatraPDF niedostępny: fallback na ShellExecute "print" verb
|
||||
- Ustaw rozmiar papieru: A6 (105×148mm) lub custom size z drukarki
|
||||
- Po wydruku: usuń temp file
|
||||
|
||||
4. PollingService:
|
||||
- Constructor: (PrintApiClient client, PrintService printService, Action<string> onStatusUpdate, Action<string> onError)
|
||||
- System.Threading.Timer z interwałem z AppSettings
|
||||
- Na każdy tick:
|
||||
a. GetPendingJobsAsync()
|
||||
b. Dla każdego job: DownloadLabelAsync → PrintPdf → MarkCompleteAsync
|
||||
c. Aktualizuj status (callback onStatusUpdate)
|
||||
d. Przy błędzie: callback onError, nie przerywaj pętli
|
||||
- Start() / Stop() / IsRunning property
|
||||
- Mutex/lock żeby nie nakładały się dwa ticki
|
||||
|
||||
5. Integracja w TrayApplicationContext:
|
||||
- Po załadowaniu ustawień: utwórz PrintApiClient + PollingService
|
||||
- onStatusUpdate → aktualizuj tooltip ikony ("Wydrukowano 3 etykiety")
|
||||
- onError → zmień ikonę na error state, tooltip z komunikatem
|
||||
- "Wstrzymaj" → PollingService.Stop(), zmień tekst menu na "Wznów"
|
||||
- Przy zmianie ustawień → restart PollingService z nowymi parametrami
|
||||
|
||||
Avoid: Nie blokuj UI thread — cała komunikacja HTTP i drukowanie async
|
||||
Avoid: Nie ignoruj błędów — każdy failed print musi być widoczny
|
||||
Avoid: Nie oznaczaj jako complete jeśli druk nie powiódł się
|
||||
</action>
|
||||
<verify>
|
||||
- `dotnet build` przechodzi
|
||||
- Aplikacja łączy się z API (test z prawdziwym kluczem)
|
||||
- Polling pobiera pending jobs
|
||||
- Etykieta drukuje się na wskazanej drukarce
|
||||
- Po wydruku job oznaczany jako complete w UI orderPRO
|
||||
- Błąd API nie crashuje aplikacji
|
||||
</verify>
|
||||
<done>AC-3 satisfied (polling), AC-4 satisfied (drukowanie), AC-5 satisfied (obsługa błędów)</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
Aplikacja Windows OrderPROPrint — system tray, polling API, drukowanie etykiet A6.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Otwórz `clients/windows/OrderPROPrint/` w Visual Studio lub uruchom `dotnet run`
|
||||
2. Sprawdź: ikona pojawia się w system tray
|
||||
3. Prawy klik → "Ustawienia" → wpisz URL + klucz API + wybierz drukarkę
|
||||
4. Kliknij "Testuj połączenie" → powinno być "Połączono ✓"
|
||||
5. Zapisz ustawienia, zamknij formularz
|
||||
6. W orderPRO: zlecij wydruk etykiety (przycisk "Drukuj" w widoku przesyłki)
|
||||
7. Poczekaj na polling (domyślnie 10s)
|
||||
8. Sprawdź: etykieta wydrukowana na drukarce Xprinter
|
||||
9. Sprawdź: w orderPRO kolejka wydruku → status "completed"
|
||||
10. Sprawdź: tooltip ikony pokazuje liczbę wydrukowanych
|
||||
11. Odłącz internet → sprawdź czy ikona zmienia się na "error"
|
||||
12. Podłącz z powrotem → polling powinien wrócić do normy
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- src/Modules/Printing/* (backend API gotowe z fazy 18-19)
|
||||
- database/migrations/* (schemat stabilny)
|
||||
- Endpointy API — klient konsumuje istniejące, nie modyfikuje serwera
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Nie dodawaj auto-start z Windows (future enhancement)
|
||||
- Nie buduj instalatora MSI/Setup (na razie `dotnet publish`)
|
||||
- Nie dodawaj auto-update mechanizmu
|
||||
- Nie modyfikuj kodu PHP — to jest czysty C# project
|
||||
- Drukowanie tylko PDF — nie konwertuj do ZPL/EPL (drukarki termiczne z Windows driver)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `dotnet build` przechodzi bez błędów i ostrzeżeń
|
||||
- [ ] Aplikacja uruchamia się w system tray (brak głównego okna)
|
||||
- [ ] Formularz ustawień: URL, klucz API, drukarka, interwał
|
||||
- [ ] "Testuj połączenie" działa z prawdziwym API
|
||||
- [ ] Polling pobiera pending jobs z API
|
||||
- [ ] Etykieta PDF drukuje się na wybranej drukarce
|
||||
- [ ] Po wydruku job oznaczany jako complete
|
||||
- [ ] Błędy nie crashują aplikacji
|
||||
- [ ] Tooltip aktualizuje się z informacjami o statusie
|
||||
- [ ] Brak natywnych alert() — to WinForms, więc MessageBox jest OK
|
||||
- [ ] Wszystkie acceptance criteria spełnione
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie 5 AC spełnione
|
||||
- Aplikacja stabilna — działa ciągle w tle bez crash
|
||||
- Komunikacja z API poprawna (auth, polling, download, complete)
|
||||
- Druk na drukarce termicznej w formacie A6
|
||||
- Kod czytelny, SRP, async/await
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/20-windows-client/20-01-SUMMARY.md`
|
||||
</output>
|
||||
149
.paul/phases/20-windows-client/20-01-SUMMARY.md
Normal file
149
.paul/phases/20-windows-client/20-01-SUMMARY.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
phase: 20-windows-client
|
||||
plan: 01
|
||||
subsystem: desktop-app
|
||||
tags: [csharp, winforms, printing, system-tray, pdfium, thermal-printer]
|
||||
|
||||
requires:
|
||||
- phase: 18-print-queue-backend
|
||||
provides: REST API (pending, download, complete), API key auth
|
||||
- phase: 19-ui-integration
|
||||
provides: UI creating print jobs (single + bulk)
|
||||
provides:
|
||||
- Windows tray application polling API and printing labels
|
||||
- Self-contained .exe with PdfiumViewer for PDF rendering
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: [".NET 8 WinForms", "PdfiumViewer 2.13.0", "pdfium native x64"]
|
||||
patterns: [polling-service, tray-application-context, synchronization-context-ui-updates]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- clients/windows/OrderPROPrint/OrderPROPrint.sln
|
||||
- clients/windows/OrderPROPrint/OrderPROPrint.csproj
|
||||
- clients/windows/OrderPROPrint/Program.cs
|
||||
- clients/windows/OrderPROPrint/TrayApplicationContext.cs
|
||||
- clients/windows/OrderPROPrint/Models/AppSettings.cs
|
||||
- clients/windows/OrderPROPrint/Models/PrintJob.cs
|
||||
- clients/windows/OrderPROPrint/Services/PrintApiClient.cs
|
||||
- clients/windows/OrderPROPrint/Services/PrintService.cs
|
||||
- clients/windows/OrderPROPrint/Services/PollingService.cs
|
||||
- clients/windows/OrderPROPrint/Forms/SettingsForm.cs
|
||||
- clients/windows/OrderPROPrint/Forms/SettingsForm.Designer.cs
|
||||
- clients/windows/OrderPROPrint/Forms/LogForm.cs
|
||||
- clients/windows/OrderPROPrint/Forms/LogForm.Designer.cs
|
||||
|
||||
key-decisions:
|
||||
- "PdfiumViewer instead of SumatraPDF — zero external dependencies, native PDF rendering"
|
||||
- "Directory publish instead of single-file — pdfium.dll native not compatible with PublishSingleFile"
|
||||
- "SynchronizationContext.Post for thread-safe tray icon updates from polling timer"
|
||||
- "Option C (single client) — multi-client support deferred"
|
||||
|
||||
patterns-established:
|
||||
- "C# desktop app lives in clients/windows/ subdirectory of orderPRO repo"
|
||||
- "PendingJobsResponse wrapper for API JSON deserialization ({jobs: [...]})"
|
||||
|
||||
duration: ~2h
|
||||
started: 2026-03-22T20:30:00Z
|
||||
completed: 2026-03-22T22:45:00Z
|
||||
---
|
||||
|
||||
# Phase 20 Plan 01: Windows Client Summary
|
||||
|
||||
**Aplikacja C# WinForms w system tray — polling API orderPRO, automatyczne pobieranie i drukowanie etykiet PDF na drukarce termicznej via PdfiumViewer.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~2h |
|
||||
| Started | 2026-03-22 |
|
||||
| Completed | 2026-03-22 |
|
||||
| Tasks | 3 (2 auto + 1 checkpoint) |
|
||||
| Files created | 13 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Aplikacja w system tray | Pass | NotifyIcon + ContextMenuStrip z Ustawienia, Logi, Wstrzymaj, O programie, Zamknij |
|
||||
| AC-2: Konfiguracja API | Pass | SettingsForm z URL, klucz, drukarka, interwał + Test połączenia |
|
||||
| AC-3: Polling i pobieranie | Pass | Timer co N sekund, download label, mark complete |
|
||||
| AC-4: Drukowanie A6 | Pass | PdfiumViewer CreatePrintDocument z PaperSize A6 (105x148mm) |
|
||||
| AC-5: Obsługa błędów | Pass | Error icon, tooltip, logi — polling kontynuuje |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Standalone .exe (self-contained .NET 8, ~170MB z runtime)
|
||||
- System tray z menu kontekstowym i dwuklikiem na ustawienia
|
||||
- Polling z konfigurowalnym interwałem (5-300s)
|
||||
- PdfiumViewer renderuje PDF i drukuje bez zewnętrznych narzędzi
|
||||
- Okno logów z historią (ciemny motyw, Consolas, kopiowanie)
|
||||
- Thread-safe UI updates via SynchronizationContext
|
||||
- Settings w %APPDATA%/OrderPROPrint/settings.json
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Scope additions | 1 | Okno logów (request użytkownika) |
|
||||
| Auto-fixed | 3 | JSON deserialization, pdfium.dll, NotifyIcon API |
|
||||
|
||||
**Total impact:** Lepsza diagnostyka dzięki logom, stabilniejszy druk.
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. JSON deserialization mismatch**
|
||||
- **Issue:** API zwraca `{"jobs": [...]}`, klient deserializował jako `List<PrintJob>`
|
||||
- **Fix:** Dodano `PendingJobsResponse` wrapper, poprawiono property names
|
||||
- **Files:** Models/PrintJob.cs, Services/PrintApiClient.cs
|
||||
|
||||
**2. pdfium.dll missing in publish**
|
||||
- **Issue:** `PublishSingleFile` nie dołącza natywnej DLL z NuGet
|
||||
- **Fix:** Directory publish + MSBuild target `CopyPdfiumNative`
|
||||
- **Files:** OrderPROPrint.csproj
|
||||
|
||||
**3. NotifyIcon API incompatibility**
|
||||
- **Issue:** `NotifyIcon.IsDisposed` i `.Invoke()` nie istnieją w .NET 8
|
||||
- **Fix:** SynchronizationContext.Post + własny `_isDisposed` flag
|
||||
- **Files:** TrayApplicationContext.cs
|
||||
|
||||
### Scope Additions
|
||||
|
||||
**1. Okno logów (LogForm)**
|
||||
- **Requested by:** użytkownik podczas testów
|
||||
- **Purpose:** diagnostyka błędów druku
|
||||
- **Files:** Forms/LogForm.cs, Forms/LogForm.Designer.cs
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| PdfiumViewer zamiast SumatraPDF | Zero external deps, native rendering | Większy .exe ale prostsze wdrożenie |
|
||||
| Directory publish zamiast single-file | pdfium.dll native incompatible | Cały folder publish/ do skopiowania |
|
||||
| Opcja C — jeden klient | Multi-client wymaga zmian backendu | Dla wielu komputerów trzeba dodać api_key_id |
|
||||
| .NET 8 SDK zainstalowany via winget | Potrzebny do budowania projektu | Nowa zależność dev na maszynie |
|
||||
|
||||
## SonarQube Scan
|
||||
|
||||
**Not applicable** — sonar-scanner skonfigurowany tylko dla PHP. Projekt C# jest poza zakresem skanowania.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Milestone v0.7 kompletny — wszystkie 3 fazy zakończone
|
||||
- Pełny flow: zlecenie z UI → API → Windows Client → drukarka
|
||||
|
||||
**Concerns:**
|
||||
- Multi-client: wielu klientów drukuje duplikaty (opcja A/B do rozważenia w przyszłości)
|
||||
- Rozmiar .exe: 170MB (self-contained z runtime .NET)
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 20-windows-client, Plan: 01*
|
||||
*Completed: 2026-03-22*
|
||||
57
clients/windows/OrderPROPrint/Forms/LogForm.Designer.cs
generated
Normal file
57
clients/windows/OrderPROPrint/Forms/LogForm.Designer.cs
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
namespace OrderPROPrint.Forms;
|
||||
|
||||
partial class LogForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
private TextBox txtLog;
|
||||
private Button btnClear;
|
||||
private Button btnCopy;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.Text = "OrderPRO Print — Logi";
|
||||
this.Size = new Size(650, 420);
|
||||
this.StartPosition = FormStartPosition.CenterScreen;
|
||||
this.MinimumSize = new Size(400, 300);
|
||||
|
||||
txtLog = new TextBox
|
||||
{
|
||||
Multiline = true,
|
||||
ReadOnly = true,
|
||||
ScrollBars = ScrollBars.Vertical,
|
||||
Font = new Font("Consolas", 9),
|
||||
BackColor = Color.FromArgb(30, 30, 30),
|
||||
ForeColor = Color.FromArgb(220, 220, 220),
|
||||
Dock = DockStyle.Fill,
|
||||
WordWrap = true
|
||||
};
|
||||
|
||||
btnClear = new Button { Text = "Wyczyść", Size = new Size(90, 28) };
|
||||
btnClear.Click += BtnClear_Click;
|
||||
|
||||
btnCopy = new Button { Text = "Kopiuj", Size = new Size(90, 28) };
|
||||
btnCopy.Click += BtnCopy_Click;
|
||||
|
||||
var panel = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Bottom,
|
||||
Height = 38,
|
||||
FlowDirection = FlowDirection.RightToLeft,
|
||||
Padding = new Padding(5, 5, 5, 0)
|
||||
};
|
||||
panel.Controls.Add(btnClear);
|
||||
panel.Controls.Add(btnCopy);
|
||||
|
||||
this.Controls.Add(txtLog);
|
||||
this.Controls.Add(panel);
|
||||
}
|
||||
}
|
||||
88
clients/windows/OrderPROPrint/Forms/LogForm.cs
Normal file
88
clients/windows/OrderPROPrint/Forms/LogForm.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
namespace OrderPROPrint.Forms;
|
||||
|
||||
public partial class LogForm : Form
|
||||
{
|
||||
private static LogForm? _instance;
|
||||
private static readonly List<string> _logEntries = new();
|
||||
private static readonly object _logLock = new();
|
||||
|
||||
public LogForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public static void Log(string message)
|
||||
{
|
||||
var entry = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||
|
||||
lock (_logLock)
|
||||
{
|
||||
_logEntries.Add(entry);
|
||||
if (_logEntries.Count > 500)
|
||||
_logEntries.RemoveAt(0);
|
||||
}
|
||||
|
||||
if (_instance != null && !_instance.IsDisposed)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instance.Invoke(() => _instance.AppendLog(entry));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public static void ShowInstance()
|
||||
{
|
||||
if (_instance == null || _instance.IsDisposed)
|
||||
{
|
||||
_instance = new LogForm();
|
||||
|
||||
lock (_logLock)
|
||||
{
|
||||
foreach (var entry in _logEntries)
|
||||
{
|
||||
_instance.AppendLog(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_instance.Show();
|
||||
_instance.BringToFront();
|
||||
}
|
||||
|
||||
private void AppendLog(string text)
|
||||
{
|
||||
txtLog.AppendText(text + Environment.NewLine);
|
||||
}
|
||||
|
||||
private void BtnClear_Click(object sender, EventArgs e)
|
||||
{
|
||||
txtLog.Clear();
|
||||
lock (_logLock)
|
||||
{
|
||||
_logEntries.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnCopy_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(txtLog.Text))
|
||||
{
|
||||
Clipboard.SetText(txtLog.Text);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
if (e.CloseReason == CloseReason.UserClosing)
|
||||
{
|
||||
e.Cancel = true;
|
||||
Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
clients/windows/OrderPROPrint/Forms/SettingsForm.Designer.cs
generated
Normal file
69
clients/windows/OrderPROPrint/Forms/SettingsForm.Designer.cs
generated
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace OrderPROPrint.Forms;
|
||||
|
||||
partial class SettingsForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
private TextBox txtApiUrl;
|
||||
private TextBox txtApiKey;
|
||||
private ComboBox cmbPrinter;
|
||||
private NumericUpDown nudPollInterval;
|
||||
private Button btnTestConnection;
|
||||
private Button btnSave;
|
||||
private Button btnCancel;
|
||||
private Label lblStatus;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.Text = "OrderPRO Print — Ustawienia";
|
||||
this.Size = new Size(450, 340);
|
||||
this.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
var lblApiUrl = new Label { Text = "URL API:", Location = new Point(15, 20), AutoSize = true };
|
||||
txtApiUrl = new TextBox { Location = new Point(15, 40), Size = new Size(400, 23) };
|
||||
txtApiUrl.PlaceholderText = "https://orderpro.projectpro.pl";
|
||||
|
||||
var lblApiKey = new Label { Text = "Klucz API:", Location = new Point(15, 75), AutoSize = true };
|
||||
txtApiKey = new TextBox { Location = new Point(15, 95), Size = new Size(310, 23), UseSystemPasswordChar = true };
|
||||
|
||||
btnTestConnection = new Button { Text = "Testuj", Location = new Point(335, 94), Size = new Size(80, 25) };
|
||||
btnTestConnection.Click += BtnTestConnection_Click;
|
||||
|
||||
lblStatus = new Label { Location = new Point(15, 123), Size = new Size(400, 18), ForeColor = Color.Gray, Text = "" };
|
||||
|
||||
var lblPrinter = new Label { Text = "Drukarka:", Location = new Point(15, 150), AutoSize = true };
|
||||
cmbPrinter = new ComboBox { Location = new Point(15, 170), Size = new Size(400, 23), DropDownStyle = ComboBoxStyle.DropDownList };
|
||||
|
||||
var lblInterval = new Label { Text = "Interwał odpytywania (sekundy):", Location = new Point(15, 205), AutoSize = true };
|
||||
nudPollInterval = new NumericUpDown { Location = new Point(15, 225), Size = new Size(80, 23), Minimum = 5, Maximum = 300, Value = 10 };
|
||||
|
||||
btnSave = new Button { Text = "Zapisz", Location = new Point(230, 265), Size = new Size(90, 30) };
|
||||
btnSave.Click += BtnSave_Click;
|
||||
|
||||
btnCancel = new Button { Text = "Anuluj", Location = new Point(325, 265), Size = new Size(90, 30) };
|
||||
btnCancel.Click += BtnCancel_Click;
|
||||
|
||||
this.Controls.AddRange(new Control[] {
|
||||
lblApiUrl, txtApiUrl,
|
||||
lblApiKey, txtApiKey, btnTestConnection,
|
||||
lblStatus,
|
||||
lblPrinter, cmbPrinter,
|
||||
lblInterval, nudPollInterval,
|
||||
btnSave, btnCancel
|
||||
});
|
||||
|
||||
this.AcceptButton = btnSave;
|
||||
this.CancelButton = btnCancel;
|
||||
}
|
||||
}
|
||||
104
clients/windows/OrderPROPrint/Forms/SettingsForm.cs
Normal file
104
clients/windows/OrderPROPrint/Forms/SettingsForm.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Drawing.Printing;
|
||||
using OrderPROPrint.Models;
|
||||
using OrderPROPrint.Services;
|
||||
|
||||
namespace OrderPROPrint.Forms;
|
||||
|
||||
public partial class SettingsForm : Form
|
||||
{
|
||||
private readonly AppSettings _settings;
|
||||
|
||||
public SettingsForm(AppSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
InitializeComponent();
|
||||
LoadSettings();
|
||||
LoadPrinters();
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
txtApiUrl.Text = _settings.ApiUrl;
|
||||
txtApiKey.Text = _settings.ApiKey;
|
||||
nudPollInterval.Value = Math.Clamp(_settings.PollIntervalSeconds, 5, 300);
|
||||
}
|
||||
|
||||
private void LoadPrinters()
|
||||
{
|
||||
cmbPrinter.Items.Clear();
|
||||
foreach (string printer in PrinterSettings.InstalledPrinters)
|
||||
{
|
||||
cmbPrinter.Items.Add(printer);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_settings.PrinterName) && cmbPrinter.Items.Contains(_settings.PrinterName))
|
||||
{
|
||||
cmbPrinter.SelectedItem = _settings.PrinterName;
|
||||
}
|
||||
else if (cmbPrinter.Items.Count > 0)
|
||||
{
|
||||
cmbPrinter.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async void BtnTestConnection_Click(object sender, EventArgs e)
|
||||
{
|
||||
var url = txtApiUrl.Text.Trim();
|
||||
var key = txtApiKey.Text.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(key))
|
||||
{
|
||||
lblStatus.ForeColor = Color.Red;
|
||||
lblStatus.Text = "Wypełnij URL i klucz API";
|
||||
return;
|
||||
}
|
||||
|
||||
lblStatus.ForeColor = Color.Gray;
|
||||
lblStatus.Text = "Testowanie...";
|
||||
btnTestConnection.Enabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
var client = new PrintApiClient(url, key);
|
||||
var success = await client.TestConnectionAsync();
|
||||
|
||||
if (success)
|
||||
{
|
||||
lblStatus.ForeColor = Color.Green;
|
||||
lblStatus.Text = "Połączono \u2713";
|
||||
}
|
||||
else
|
||||
{
|
||||
lblStatus.ForeColor = Color.Red;
|
||||
lblStatus.Text = "Błąd autoryzacji — sprawdź klucz API";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lblStatus.ForeColor = Color.Red;
|
||||
lblStatus.Text = $"Błąd: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
btnTestConnection.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnSave_Click(object sender, EventArgs e)
|
||||
{
|
||||
_settings.ApiUrl = txtApiUrl.Text.Trim().TrimEnd('/');
|
||||
_settings.ApiKey = txtApiKey.Text.Trim();
|
||||
_settings.PrinterName = cmbPrinter.SelectedItem?.ToString() ?? "";
|
||||
_settings.PollIntervalSeconds = (int)nudPollInterval.Value;
|
||||
_settings.Save();
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void BtnCancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
45
clients/windows/OrderPROPrint/Models/AppSettings.cs
Normal file
45
clients/windows/OrderPROPrint/Models/AppSettings.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace OrderPROPrint.Models;
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public string ApiUrl { get; set; } = "";
|
||||
public string ApiKey { get; set; } = "";
|
||||
public string PrinterName { get; set; } = "";
|
||||
public int PollIntervalSeconds { get; set; } = 10;
|
||||
|
||||
private static string SettingsDir =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "OrderPROPrint");
|
||||
|
||||
private static string SettingsPath =>
|
||||
Path.Combine(SettingsDir, "settings.json");
|
||||
|
||||
public bool IsConfigured =>
|
||||
!string.IsNullOrWhiteSpace(ApiUrl) && !string.IsNullOrWhiteSpace(ApiKey) && !string.IsNullOrWhiteSpace(PrinterName);
|
||||
|
||||
public static AppSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(SettingsPath))
|
||||
{
|
||||
var json = File.ReadAllText(SettingsPath);
|
||||
return JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Return defaults on any error
|
||||
}
|
||||
|
||||
return new AppSettings();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
Directory.CreateDirectory(SettingsDir);
|
||||
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(SettingsPath, json);
|
||||
}
|
||||
}
|
||||
24
clients/windows/OrderPROPrint/Models/PrintJob.cs
Normal file
24
clients/windows/OrderPROPrint/Models/PrintJob.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace OrderPROPrint.Models;
|
||||
|
||||
public class PendingJobsResponse
|
||||
{
|
||||
[JsonPropertyName("jobs")]
|
||||
public List<PrintJob> Jobs { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PrintJob
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("order_number")]
|
||||
public string OrderNumber { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("tracking_number")]
|
||||
public string TrackingNumber { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public string CreatedAt { get; set; } = "";
|
||||
}
|
||||
31
clients/windows/OrderPROPrint/OrderPROPrint.csproj
Normal file
31
clients/windows/OrderPROPrint/OrderPROPrint.csproj
Normal file
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AssemblyName>OrderPROPrint</AssemblyName>
|
||||
<Product>OrderPRO Print</Product>
|
||||
<Version>1.0.0</Version>
|
||||
<Description>Klient zdalnego drukowania etykiet orderPRO</Description>
|
||||
<NoWarn>NU1701</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PdfiumViewer" Version="2.13.0" />
|
||||
<PackageReference Include="PdfiumViewer.Native.x86_64.v8-xfa" Version="2018.4.8.256" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyPdfiumNative" AfterTargets="Publish">
|
||||
<Copy SourceFiles="$(NuGetPackageRoot)pdfiumviewer.native.x86_64.v8-xfa\2018.4.8.256\Build\x64\pdfium.dll" DestinationFolder="$(PublishDir)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
8
clients/windows/OrderPROPrint/OrderPROPrint.csproj.user
Normal file
8
clients/windows/OrderPROPrint/OrderPROPrint.csproj.user
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Compile Update="Form1.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
11
clients/windows/OrderPROPrint/Program.cs
Normal file
11
clients/windows/OrderPROPrint/Program.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace OrderPROPrint;
|
||||
|
||||
static class Program
|
||||
{
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
ApplicationConfiguration.Initialize();
|
||||
Application.Run(new TrayApplicationContext());
|
||||
}
|
||||
}
|
||||
159
clients/windows/OrderPROPrint/Services/PollingService.cs
Normal file
159
clients/windows/OrderPROPrint/Services/PollingService.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using OrderPROPrint.Forms;
|
||||
|
||||
namespace OrderPROPrint.Services;
|
||||
|
||||
public class PollingService
|
||||
{
|
||||
private readonly PrintApiClient _apiClient;
|
||||
private readonly PrintService _printService;
|
||||
private readonly string _printerName;
|
||||
private readonly int _intervalSeconds;
|
||||
private readonly Action<string> _onStatusUpdate;
|
||||
private readonly Action<string> _onError;
|
||||
|
||||
private System.Threading.Timer? _timer;
|
||||
private bool _isProcessing;
|
||||
private int _totalPrinted;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public PollingService(
|
||||
PrintApiClient apiClient,
|
||||
PrintService printService,
|
||||
string printerName,
|
||||
int intervalSeconds,
|
||||
Action<string> onStatusUpdate,
|
||||
Action<string> onError)
|
||||
{
|
||||
_apiClient = apiClient;
|
||||
_printService = printService;
|
||||
_printerName = printerName;
|
||||
_intervalSeconds = Math.Max(intervalSeconds, 5);
|
||||
_onStatusUpdate = onStatusUpdate;
|
||||
_onError = onError;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (IsRunning) return;
|
||||
|
||||
IsRunning = true;
|
||||
_timer = new System.Threading.Timer(
|
||||
async _ => await PollAsync(),
|
||||
null,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(_intervalSeconds)
|
||||
);
|
||||
|
||||
_onStatusUpdate($"Aktywne (co {_intervalSeconds}s)");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
IsRunning = false;
|
||||
_timer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
private async Task PollAsync()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_isProcessing) return;
|
||||
_isProcessing = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var jobs = await _apiClient.GetPendingJobsAsync();
|
||||
LogForm.Log($"Polling: znaleziono {jobs.Count} zleceń");
|
||||
|
||||
if (jobs.Count == 0)
|
||||
{
|
||||
_onStatusUpdate($"Brak zleceń (wydrukowano: {_totalPrinted})");
|
||||
return;
|
||||
}
|
||||
|
||||
int printed = 0;
|
||||
int failed = 0;
|
||||
|
||||
string lastError = "";
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogForm.Log($"Job {job.Id}: pobieranie etykiety...");
|
||||
var labelBytes = await _apiClient.DownloadLabelAsync(job.Id);
|
||||
LogForm.Log($"Job {job.Id}: pobrano {labelBytes.Length} bajtów");
|
||||
|
||||
if (labelBytes.Length == 0)
|
||||
{
|
||||
failed++;
|
||||
lastError = $"Job {job.Id}: pusty plik etykiety";
|
||||
LogForm.Log($"BŁĄD: {lastError}");
|
||||
continue;
|
||||
}
|
||||
|
||||
LogForm.Log($"Job {job.Id}: drukowanie na '{_printerName}'...");
|
||||
bool success = _printService.PrintPdf(labelBytes, _printerName);
|
||||
|
||||
if (success)
|
||||
{
|
||||
await _apiClient.MarkCompleteAsync(job.Id);
|
||||
printed++;
|
||||
_totalPrinted++;
|
||||
LogForm.Log($"Job {job.Id}: wydrukowano i oznaczono jako complete ✓");
|
||||
}
|
||||
else
|
||||
{
|
||||
failed++;
|
||||
lastError = $"Job {job.Id}: druk nie powiódł się";
|
||||
LogForm.Log($"BŁĄD: {lastError}");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
failed++;
|
||||
lastError = $"Job {job.Id}: HTTP {ex.StatusCode} — {ex.Message}";
|
||||
LogForm.Log($"BŁĄD: {lastError}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failed++;
|
||||
lastError = $"Job {job.Id}: {ex.GetType().Name} — {ex.Message}";
|
||||
LogForm.Log($"BŁĄD: {lastError}");
|
||||
}
|
||||
}
|
||||
|
||||
if (failed > 0)
|
||||
{
|
||||
_onStatusUpdate($"Wydrukowano: {printed}, błędy: {failed} (łącznie: {_totalPrinted})");
|
||||
}
|
||||
else
|
||||
{
|
||||
_onStatusUpdate($"Wydrukowano {printed} etykiet (łącznie: {_totalPrinted})");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_onError($"API niedostępne: {ex.Message}");
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
_onError("Timeout połączenia z API");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onError(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
clients/windows/OrderPROPrint/Services/PrintApiClient.cs
Normal file
61
clients/windows/OrderPROPrint/Services/PrintApiClient.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using OrderPROPrint.Models;
|
||||
|
||||
namespace OrderPROPrint.Services;
|
||||
|
||||
public class PrintApiClient : IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public PrintApiClient(string apiUrl, string apiKey)
|
||||
{
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(apiUrl.TrimEnd('/') + "/"),
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Api-Key", apiKey);
|
||||
}
|
||||
|
||||
public async Task<List<PrintJob>> GetPendingJobsAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("api/print/jobs/pending");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonSerializer.Deserialize<PendingJobsResponse>(json);
|
||||
return result?.Jobs ?? new List<PrintJob>();
|
||||
}
|
||||
|
||||
public async Task<byte[]> DownloadLabelAsync(int jobId)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/print/jobs/{jobId}/download");
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsByteArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> MarkCompleteAsync(int jobId)
|
||||
{
|
||||
var response = await _httpClient.PostAsync($"api/print/jobs/{jobId}/complete", null);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task<bool> TestConnectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync("api/print/jobs/pending");
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
32
clients/windows/OrderPROPrint/Services/PrintService.cs
Normal file
32
clients/windows/OrderPROPrint/Services/PrintService.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Drawing.Printing;
|
||||
using PdfiumViewer;
|
||||
|
||||
namespace OrderPROPrint.Services;
|
||||
|
||||
public class PrintService
|
||||
{
|
||||
public bool PrintPdf(byte[] pdfBytes, string printerName)
|
||||
{
|
||||
using var stream = new MemoryStream(pdfBytes);
|
||||
using var pdfDocument = PdfDocument.Load(stream);
|
||||
|
||||
using var printDocument = pdfDocument.CreatePrintDocument();
|
||||
printDocument.PrinterSettings.PrinterName = printerName;
|
||||
printDocument.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0);
|
||||
|
||||
// A6 = 105x148mm = 413x583 hundredths of inch
|
||||
printDocument.DefaultPageSettings.PaperSize = new PaperSize("A6", 413, 583);
|
||||
|
||||
printDocument.PrintController = new StandardPrintController();
|
||||
|
||||
try
|
||||
{
|
||||
printDocument.Print();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
174
clients/windows/OrderPROPrint/TrayApplicationContext.cs
Normal file
174
clients/windows/OrderPROPrint/TrayApplicationContext.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using OrderPROPrint.Forms;
|
||||
using OrderPROPrint.Models;
|
||||
using OrderPROPrint.Services;
|
||||
|
||||
namespace OrderPROPrint;
|
||||
|
||||
public class TrayApplicationContext : ApplicationContext
|
||||
{
|
||||
private readonly NotifyIcon _trayIcon;
|
||||
private readonly ToolStripMenuItem _pauseMenuItem;
|
||||
private readonly SynchronizationContext _syncContext;
|
||||
private PollingService? _pollingService;
|
||||
private AppSettings _settings;
|
||||
private bool _isPaused;
|
||||
private bool _isDisposed;
|
||||
|
||||
public TrayApplicationContext()
|
||||
{
|
||||
_syncContext = SynchronizationContext.Current ?? new SynchronizationContext();
|
||||
_settings = AppSettings.Load();
|
||||
|
||||
_pauseMenuItem = new ToolStripMenuItem("Wstrzymaj", null, OnPauseToggle);
|
||||
|
||||
var contextMenu = new ContextMenuStrip();
|
||||
contextMenu.Items.Add("Ustawienia", null, OnSettings);
|
||||
contextMenu.Items.Add("Logi", null, OnLogs);
|
||||
contextMenu.Items.Add(_pauseMenuItem);
|
||||
contextMenu.Items.Add(new ToolStripSeparator());
|
||||
contextMenu.Items.Add("O programie", null, OnAbout);
|
||||
contextMenu.Items.Add("Zamknij", null, OnExit);
|
||||
|
||||
_trayIcon = new NotifyIcon
|
||||
{
|
||||
Icon = SystemIcons.Application,
|
||||
Text = "OrderPRO Print — Oczekiwanie",
|
||||
ContextMenuStrip = contextMenu,
|
||||
Visible = true
|
||||
};
|
||||
_trayIcon.DoubleClick += OnSettings;
|
||||
|
||||
if (_settings.IsConfigured)
|
||||
{
|
||||
StartPolling();
|
||||
}
|
||||
else
|
||||
{
|
||||
_trayIcon.ShowBalloonTip(3000, "OrderPRO Print", "Skonfiguruj połączenie w Ustawieniach.", ToolTipIcon.Info);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartPolling()
|
||||
{
|
||||
_pollingService?.Stop();
|
||||
|
||||
var apiClient = new PrintApiClient(_settings.ApiUrl, _settings.ApiKey);
|
||||
var printService = new PrintService();
|
||||
|
||||
_pollingService = new PollingService(
|
||||
apiClient,
|
||||
printService,
|
||||
_settings.PrinterName,
|
||||
_settings.PollIntervalSeconds,
|
||||
OnStatusUpdate,
|
||||
OnError
|
||||
);
|
||||
_pollingService.Start();
|
||||
_isPaused = false;
|
||||
_pauseMenuItem.Text = "Wstrzymaj";
|
||||
SetNormalState();
|
||||
}
|
||||
|
||||
private void OnStatusUpdate(string message)
|
||||
{
|
||||
LogForm.Log(message);
|
||||
if (_isDisposed) return;
|
||||
_syncContext.Post(_ =>
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
_trayIcon.Text = TruncateTooltip($"OrderPRO Print — {message}");
|
||||
_trayIcon.Icon = SystemIcons.Application;
|
||||
}, null);
|
||||
}
|
||||
|
||||
private void OnError(string message)
|
||||
{
|
||||
LogForm.Log($"BŁĄD: {message}");
|
||||
if (_isDisposed) return;
|
||||
_syncContext.Post(_ =>
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
_trayIcon.Text = TruncateTooltip($"OrderPRO Print — Błąd: {message}");
|
||||
_trayIcon.Icon = SystemIcons.Error;
|
||||
}, null);
|
||||
}
|
||||
|
||||
private void OnLogs(object? sender, EventArgs e)
|
||||
{
|
||||
LogForm.ShowInstance();
|
||||
}
|
||||
|
||||
private void SetNormalState()
|
||||
{
|
||||
_trayIcon.Icon = SystemIcons.Application;
|
||||
_trayIcon.Text = "OrderPRO Print — Aktywne";
|
||||
}
|
||||
|
||||
private void OnSettings(object? sender, EventArgs e)
|
||||
{
|
||||
using var form = new SettingsForm(_settings);
|
||||
if (form.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
_settings = AppSettings.Load();
|
||||
if (_settings.IsConfigured)
|
||||
{
|
||||
StartPolling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPauseToggle(object? sender, EventArgs e)
|
||||
{
|
||||
if (_pollingService == null) return;
|
||||
|
||||
if (_isPaused)
|
||||
{
|
||||
_pollingService.Start();
|
||||
_isPaused = false;
|
||||
_pauseMenuItem.Text = "Wstrzymaj";
|
||||
SetNormalState();
|
||||
}
|
||||
else
|
||||
{
|
||||
_pollingService.Stop();
|
||||
_isPaused = true;
|
||||
_pauseMenuItem.Text = "Wznów";
|
||||
_trayIcon.Text = "OrderPRO Print — Wstrzymane";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAbout(object? sender, EventArgs e)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"OrderPRO Print v1.0\n\nKlient zdalnego drukowania etykiet.\nPobiera zlecenia z orderPRO i drukuje na drukarce termicznej.",
|
||||
"O programie",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information
|
||||
);
|
||||
}
|
||||
|
||||
private void OnExit(object? sender, EventArgs e)
|
||||
{
|
||||
_pollingService?.Stop();
|
||||
_trayIcon.Visible = false;
|
||||
_trayIcon.Dispose();
|
||||
Application.Exit();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_isDisposed = true;
|
||||
_pollingService?.Stop();
|
||||
_trayIcon.Visible = false;
|
||||
_trayIcon.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private static string TruncateTooltip(string text)
|
||||
{
|
||||
return text.Length > 63 ? text[..63] : text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"OrderPROPrint/1.0.0": {
|
||||
"dependencies": {
|
||||
"PdfiumViewer": "2.13.0",
|
||||
"PdfiumViewer.Native.x86_64.v8-xfa": "2018.4.8.256"
|
||||
},
|
||||
"runtime": {
|
||||
"OrderPROPrint.dll": {}
|
||||
}
|
||||
},
|
||||
"PdfiumViewer/2.13.0": {
|
||||
"runtime": {
|
||||
"lib/net20/PdfiumViewer.dll": {
|
||||
"assemblyVersion": "2.13.0.0",
|
||||
"fileVersion": "2.13.0.0"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"lib/net20/nl/PdfiumViewer.resources.dll": {
|
||||
"locale": "nl"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PdfiumViewer.Native.x86_64.v8-xfa/2018.4.8.256": {}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"OrderPROPrint/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"PdfiumViewer/2.13.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Nw4owBmJzDVXoMRjCnqPbET07CPOawwhcNjt+PyRaSxONN9bsFF+cm9kcMsDLEAEaxsiDJo5jls8bCQjvjidkA==",
|
||||
"path": "pdfiumviewer/2.13.0",
|
||||
"hashPath": "pdfiumviewer.2.13.0.nupkg.sha512"
|
||||
},
|
||||
"PdfiumViewer.Native.x86_64.v8-xfa/2018.4.8.256": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-0RQwmLMPKOHBRqRK+NPMR2H5EQ4KlYO+LnT3Y2lByVUa5/JTPCMswL46JEXX+/MAfJtCpS2PzJvy6QzpH2nuIw==",
|
||||
"path": "pdfiumviewer.native.x86_64.v8-xfa/2018.4.8.256",
|
||||
"hashPath": "pdfiumviewer.native.x86_64.v8-xfa.2018.4.8.256.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"includedFrameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.25"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App",
|
||||
"version": "8.0.25"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user