diff --git a/src/CamBooth/.run/Publish CamBooth.App to folder.run.xml b/src/CamBooth/.run/Publish CamBooth.App to folder.run.xml
new file mode 100644
index 0000000..f029761
--- /dev/null
+++ b/src/CamBooth/.run/Publish CamBooth.App to folder.run.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/CamBooth/CamBooth.App/App.xaml.cs b/src/CamBooth/CamBooth.App/App.xaml.cs
index 345a561..ec4a2e5 100644
--- a/src/CamBooth/CamBooth.App/App.xaml.cs
+++ b/src/CamBooth/CamBooth.App/App.xaml.cs
@@ -1,4 +1,4 @@
-using System.IO;
+using System.IO;
using System.Windows;
using CamBooth.App.Core.AppSettings;
@@ -51,6 +51,7 @@ public partial class App : Application
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
// Zuerst den Provider bauen, um AppSettings zu laden
@@ -104,7 +105,42 @@ public partial class App : Application
_serviceProvider = services.BuildServiceProvider();
+ // Starte UploadQueueService beim Start
+ try
+ {
+ var uploadQueueService = _serviceProvider.GetRequiredService();
+ uploadQueueService.Start();
+
+ // Scan für fehlgeschlagene Uploads beim Start
+ uploadQueueService.ScanAndQueueFailedUploads();
+
+ logger.Info("UploadQueueService initialisiert und gestartet");
+ }
+ catch (Exception ex)
+ {
+ logger.Error($"Fehler beim Start des UploadQueueService: {ex.Message}");
+ }
+
var mainWindow = _serviceProvider.GetRequiredService();
mainWindow.Show();
}
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ // Stoppe UploadQueueService beim Beenden der App
+ try
+ {
+ var uploadQueueService = _serviceProvider?.GetService();
+ if (uploadQueueService != null)
+ {
+ uploadQueueService.StopAsync().Wait(TimeSpan.FromSeconds(10));
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Fehler beim Stoppen des UploadQueueService: {ex.Message}");
+ }
+
+ base.OnExit(e);
+ }
}
\ No newline at end of file
diff --git a/src/CamBooth/CamBooth.App/CamBooth.App.csproj b/src/CamBooth/CamBooth.App/CamBooth.App.csproj
index 208bc17..de2493b 100644
--- a/src/CamBooth/CamBooth.App/CamBooth.App.csproj
+++ b/src/CamBooth/CamBooth.App/CamBooth.App.csproj
@@ -9,62 +9,67 @@
- x86
+ x86
- x86
+ x86
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
- Always
-
-
- Always
-
+
+ Always
+
+
+ Always
+
-
- MSBuild:Compile
-
-
- MSBuild:Compile
-
-
- MSBuild:Compile
-
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
diff --git a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json
index 2735108..c6f4671 100644
--- a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json
+++ b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json
@@ -44,7 +44,7 @@
"ApiUrl": "https://gallery.grimma-fotobox.de",
"Username": "itob",
"Password": "VfVyqal&Nv8U&P",
- "DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",
+ "DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR",
"AutoUploadEnabled": true
},
"ConnectionStrings": {
diff --git a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json
index 3fa6783..a994c1b 100644
--- a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json
+++ b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json
@@ -44,8 +44,8 @@
"ApiUrl": "https://gallery.grimma-fotobox.de",
"Username": "itob",
"Password": "VfVyqal&Nv8U&P",
- "DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",
- "AutoUploadEnabled": false
+ "DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR",
+ "AutoUploadEnabled": true
},
"ConnectionStrings": {
"DefaultConnection": "Server=myServer;Database=myDB;User Id=myUser;Password=myPassword;"
diff --git a/src/CamBooth/CamBooth.App/Core/GenericOverlayWindow.xaml b/src/CamBooth/CamBooth.App/Core/GenericOverlayWindow.xaml
index 5498f61..3bd6480 100644
--- a/src/CamBooth/CamBooth.App/Core/GenericOverlayWindow.xaml
+++ b/src/CamBooth/CamBooth.App/Core/GenericOverlayWindow.xaml
@@ -6,9 +6,19 @@
xmlns:local="clr-namespace:CamBooth.App.Core"
mc:Ignorable="d"
Title="GenericOverlayWindow" Height="350" Width="600">
+
+
+
Hoppla
Sorry, da ging was schief!
-
+
diff --git a/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs b/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs
index 439998f..d1636e5 100644
--- a/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs
+++ b/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -68,10 +68,16 @@ public class Logger : IDisposable
public Logger(IConfiguration configuration)
{
var logLevel = configuration["LoggingSettings:LogLevel"] ?? "Information";
- var logDirectory = configuration["LoggingSettings:LogDirectory"] ?? "Logs";
+ var logDirectoryInput = configuration["LoggingSettings:LogDirectory"] ?? "Logs";
var remoteServerUrl = configuration["LoggingSettings:RemoteServerUrl"];
var remoteServerApiKey = configuration["LoggingSettings:RemoteServerApiKey"];
+ // Konvertiere zu absolutem Pfad
+ var logDirectory = Path.IsPathRooted(logDirectoryInput)
+ ? logDirectoryInput
+ : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, logDirectoryInput);
+
+ // Erstelle Verzeichnis, falls nicht vorhanden
if (!Directory.Exists(logDirectory))
{
Directory.CreateDirectory(logDirectory);
diff --git a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs
index 0cfcf23..bc6766d 100644
--- a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs
+++ b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs
@@ -22,6 +22,8 @@ public class CameraService : IDisposable
private readonly LycheeUploadService _lycheeUploadService;
+ private readonly UploadQueueService _uploadQueueService;
+
private readonly ICanonAPI _APIHandler;
private CameraValue[] AvList;
@@ -47,6 +49,7 @@ public class CameraService : IDisposable
AppSettingsService appSettings,
PictureGalleryService pictureGalleryService,
LycheeUploadService lycheeUploadService,
+ UploadQueueService uploadQueueService,
ICamera mainCamera,
ICanonAPI APIHandler)
{
@@ -54,6 +57,7 @@ public class CameraService : IDisposable
this._appSettings = appSettings;
this._pictureGalleryService = pictureGalleryService;
this._lycheeUploadService = lycheeUploadService;
+ this._uploadQueueService = uploadQueueService;
this._mainCamera = mainCamera;
this._APIHandler = APIHandler;
try
@@ -89,21 +93,43 @@ public class CameraService : IDisposable
try
{
this.RefreshCamera();
- List cameraList = this._APIHandler.GetCameraList();
- if (cameraList.Any())
+
+ // Retry logic for camera detection (some systems need time to initialize)
+ int maxRetries = 3;
+ int retryDelay = 500; // milliseconds
+
+ for (int attempt = 0; attempt < maxRetries; attempt++)
{
- this.OpenSession();
- this.SetSettingSaveToComputer();
- this.StarLiveView();
+ if (this.CamList != null && this.CamList.Any())
+ {
+ break;
+ }
+
+ if (attempt < maxRetries - 1)
+ {
+ this._logger.Info($"No cameras found on attempt {attempt + 1}/{maxRetries}, retrying in {retryDelay}ms...");
+ System.Threading.Thread.Sleep(retryDelay);
+ this.RefreshCamera();
+ }
}
- else
+
+ if (this.CamList == null || !this.CamList.Any())
{
this.ReportError("No cameras / devices found");
- return;
+ throw new InvalidOperationException("No cameras / devices found after multiple attempts");
}
- string cameraDeviceNames = string.Join(", ", cameraList.Select(cam => cam.DeviceName));
+ this._logger.Info($"Found {this.CamList.Count} camera(s)");
+ string cameraDeviceNames = string.Join(", ", this.CamList.Select(cam => cam.DeviceName));
this._logger.Info(cameraDeviceNames);
+
+ // Update _mainCamera reference to the freshly detected camera
+ this._mainCamera = this.CamList[0];
+ this._logger.Info($"Selected camera: {this._mainCamera.DeviceName}");
+
+ this.OpenSession();
+ this.SetSettingSaveToComputer();
+ this.StarLiveView();
}
catch (Exception ex)
{
@@ -161,13 +187,11 @@ public class CameraService : IDisposable
{
try
{
- if (this.CamList == null || this.CamList.Count == 0)
+ if (this._mainCamera == null)
{
- throw new InvalidOperationException("No cameras available in camera list");
+ throw new InvalidOperationException("Camera reference is null. Make sure ConnectCamera is called first.");
}
- this._mainCamera = this.CamList[0];
-
// Check if session is already open
if (this._mainCamera.SessionOpen)
{
@@ -377,34 +401,9 @@ public class CameraService : IDisposable
this._pictureGalleryService.LoadThumbnailsToCache();
});
- // Auto-Upload zu Lychee, falls aktiviert
- if (this._appSettings.LycheeAutoUploadEnabled)
- {
- this._logger.Info("Auto-Upload aktiviert. Starte Upload zu Lychee...");
-
- // Upload im Hintergrund, damit die Fotobox nicht blockiert wird
- _ = Task.Run(async () =>
- {
- try
- {
- var albumId = this._appSettings.LycheeDefaultAlbumId;
- var uploadSuccess = await this._lycheeUploadService.UploadImageAsync(savedPhotoPath, albumId);
-
- if (uploadSuccess)
- {
- this._logger.Info($"✅ Lychee-Upload erfolgreich: {Info.FileName}");
- }
- else
- {
- this._logger.Warning($"⚠️ Lychee-Upload fehlgeschlagen: {Info.FileName}");
- }
- }
- catch (Exception ex)
- {
- this._logger.Error($"❌ Fehler beim Lychee-Upload: {ex.Message}");
- }
- });
- }
+ // Füge neues Foto zur Upload-Queue hinzu (wenn Auto-Upload aktiviert)
+ this._uploadQueueService.QueueNewPhoto(savedPhotoPath);
+ this._logger.Info($"Foto zur Upload-Queue hinzugefügt: {Info.FileName}");
}
catch (Exception ex)
{
diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml
index bbe7d6c..d383a79 100644
--- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml
+++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml
@@ -75,11 +75,9 @@
x:Name="ConnectButton"
Content="Verbinden"
Click="ConnectButton_Click"
+ Style="{StaticResource PrimaryActionButtonStyle}"
Width="140"
- Height="45"
- Background="#D4AF37"
- Foreground="#1F1A00"
- FontWeight="SemiBold" />
+ Height="45" />
@@ -165,18 +163,16 @@
+ Margin="0,0,10,0" />
+ Height="45" />
diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadService.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadService.cs
index 6fe8365..44e3f3f 100644
--- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadService.cs
+++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadService.cs
@@ -1,10 +1,11 @@
-using System.IO;
+using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
+using System.Windows.Media.Imaging;
using CamBooth.App.Core.AppSettings;
using CamBooth.App.Core.Logging;
@@ -18,6 +19,7 @@ public class LycheeUploadService : IDisposable
private readonly AppSettingsService _appSettings;
private readonly Logger _logger;
private readonly HttpClient _httpClient;
+ private readonly QRCodeGenerationService _qrCodeGenerationService;
private readonly CookieContainer _cookieContainer;
private readonly HttpClientHandler _httpClientHandler;
private string? _csrfToken;
@@ -27,6 +29,7 @@ public class LycheeUploadService : IDisposable
{
_appSettings = appSettings;
_logger = logger;
+ _qrCodeGenerationService = new QRCodeGenerationService(logger);
// CookieContainer für Session-Management
_cookieContainer = new CookieContainer();
@@ -47,6 +50,11 @@ public class LycheeUploadService : IDisposable
///
public bool IsAuthenticated => _isAuthenticated;
+ ///
+ /// Der zuletzt generierte QR-Code
+ ///
+ public BitmapImage? LastGeneratedQRCode { get; private set; }
+
///
/// Authentifiziert sich bei Lychee
///
@@ -532,6 +540,52 @@ public class LycheeUploadService : IDisposable
}
}
+ ///
+ /// Generiert einen QR-Code für die Lychee-Galerie-URL
+ ///
+ /// BitmapImage des QR-Codes oder null bei Fehler
+ public BitmapImage? GenerateGalleryQRCode()
+ {
+ try
+ {
+ var lycheeUrl = _appSettings.LycheeApiUrl;
+ var albumId = _appSettings.LycheeDefaultAlbumId;
+
+ if (string.IsNullOrEmpty(lycheeUrl))
+ {
+ _logger.Error("Lychee-URL ist nicht konfiguriert. Kann QR-Code nicht generieren.");
+ return null;
+ }
+
+ if (string.IsNullOrEmpty(albumId))
+ {
+ _logger.Warning("Lychee DefaultAlbumId ist nicht konfiguriert. QR-Code zeigt nur die Basis-URL.");
+ }
+
+ _logger.Info("Generiere QR-Code für Lychee-Galerie...");
+
+ // Konstruiere die Gallery-URL: ApiUrl + /Gallery + DefaultAlbumId
+ var galleryUrl = $"{lycheeUrl}/gallery/{albumId}";
+ _logger.Debug($"QR-Code URL: {galleryUrl}");
+
+ // Generiere QR-Code mit der Gallery-URL
+ var qrCode = _qrCodeGenerationService.GenerateQRCode(galleryUrl);
+
+ if (qrCode != null)
+ {
+ LastGeneratedQRCode = qrCode;
+ _logger.Info("✅ QR-Code erfolgreich generiert");
+ }
+
+ return qrCode;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Fehler beim Generieren des QR-Codes: {ex.Message}");
+ return null;
+ }
+ }
+
public void Dispose()
{
_httpClient?.Dispose();
diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeDisplayWindow.xaml b/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeDisplayWindow.xaml
new file mode 100644
index 0000000..c93b05d
--- /dev/null
+++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeDisplayWindow.xaml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeDisplayWindow.xaml.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeDisplayWindow.xaml.cs
new file mode 100644
index 0000000..ddbb9f7
--- /dev/null
+++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeDisplayWindow.xaml.cs
@@ -0,0 +1,28 @@
+using System.Windows;
+using System.Windows.Media.Imaging;
+
+namespace CamBooth.App.Features.LycheeUpload;
+
+public partial class QRCodeDisplayWindow : Window
+{
+ public QRCodeDisplayWindow()
+ {
+ InitializeComponent();
+ }
+
+ ///
+ /// Setzt den QR-Code für die Anzeige
+ ///
+ public void SetQRCode(BitmapImage qrCodeImage)
+ {
+ if (qrCodeImage != null)
+ {
+ QRCodeImage.Source = qrCodeImage;
+ }
+ }
+
+ private void CloseButton_Click(object sender, RoutedEventArgs e)
+ {
+ this.Close();
+ }
+}
diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeGenerationService.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeGenerationService.cs
new file mode 100644
index 0000000..4438d6c
--- /dev/null
+++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeGenerationService.cs
@@ -0,0 +1,108 @@
+using System.Drawing;
+using System.IO;
+using System.Windows.Media.Imaging;
+using CamBooth.App.Core.Logging;
+using QRCoder;
+
+namespace CamBooth.App.Features.LycheeUpload;
+
+///
+/// Service für die Generierung von QR-Codes als BitmapImage
+///
+public class QRCodeGenerationService
+{
+ private readonly Logger _logger;
+
+ public QRCodeGenerationService(Logger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ /// Generiert einen QR-Code basierend auf einem URL-String
+ ///
+ /// Die Daten für den QR-Code (z.B. URL)
+ /// Größe in Pixeln pro QR-Code-Modul
+ /// BitmapImage des QR-Codes oder null bei Fehler
+ public BitmapImage? GenerateQRCode(string qrData, int pixelsPerModule = 20)
+ {
+ try
+ {
+ _logger.Debug($"Generiere QR-Code für: {qrData}");
+
+ using (QRCodeGenerator qrGenerator = new QRCodeGenerator())
+ {
+ QRCodeData qrCodeData = qrGenerator.CreateQrCode(qrData, QRCodeGenerator.ECCLevel.Q);
+ using (QRCode qrCode = new QRCode(qrCodeData))
+ {
+ Bitmap qrCodeImage = qrCode.GetGraphic(pixelsPerModule, System.Drawing.Color.Black, System.Drawing.Color.White, true);
+
+ // Konvertiere Bitmap zu BitmapImage
+ BitmapImage bitmapImage = ConvertBitmapToBitmapImage(qrCodeImage);
+
+ _logger.Debug("✅ QR-Code erfolgreich generiert");
+ return bitmapImage;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Fehler beim Generieren des QR-Codes: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Konvertiert ein System.Drawing.Bitmap zu einem WPF BitmapImage
+ ///
+ private BitmapImage ConvertBitmapToBitmapImage(Bitmap bitmap)
+ {
+ using (var stream = new MemoryStream())
+ {
+ bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var bitmapImage = new BitmapImage();
+ bitmapImage.BeginInit();
+ bitmapImage.StreamSource = stream;
+ bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
+ bitmapImage.EndInit();
+ bitmapImage.Freeze();
+
+ return bitmapImage;
+ }
+ }
+
+ ///
+ /// Speichert einen QR-Code als Datei
+ ///
+ /// Die Daten für den QR-Code
+ /// Zielpath für die QR-Code-Datei
+ /// Größe in Pixeln pro QR-Code-Modul
+ /// True wenn erfolgreich, sonst False
+ public bool SaveQRCodeToFile(string qrData, string filePath, int pixelsPerModule = 20)
+ {
+ try
+ {
+ _logger.Debug($"Speichere QR-Code unter: {filePath}");
+
+ using (QRCodeGenerator qrGenerator = new QRCodeGenerator())
+ {
+ QRCodeData qrCodeData = qrGenerator.CreateQrCode(qrData, QRCodeGenerator.ECCLevel.Q);
+ using (QRCode qrCode = new QRCode(qrCodeData))
+ {
+ Bitmap qrCodeImage = qrCode.GetGraphic(pixelsPerModule, System.Drawing.Color.Black, System.Drawing.Color.White, true);
+ qrCodeImage.Save(filePath, System.Drawing.Imaging.ImageFormat.Png);
+
+ _logger.Debug("✅ QR-Code erfolgreich gespeichert");
+ return true;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Fehler beim Speichern des QR-Codes: {ex.Message}");
+ return false;
+ }
+ }
+}
diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/UploadQueueService.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/UploadQueueService.cs
new file mode 100644
index 0000000..4c3f2a2
--- /dev/null
+++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/UploadQueueService.cs
@@ -0,0 +1,288 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using CamBooth.App.Core.AppSettings;
+using CamBooth.App.Core.Logging;
+
+namespace CamBooth.App.Features.LycheeUpload;
+
+///
+/// Verwaltet die Upload-Queue und führt Uploads im Hintergrund durch
+///
+public class UploadQueueService : IDisposable
+{
+ private readonly Logger _logger;
+ private readonly AppSettingsService _appSettings;
+ private readonly LycheeUploadService _lycheeUploadService;
+ private readonly UploadTracker _uploadTracker;
+ private readonly Queue _uploadQueue;
+ private Task? _uploadTask;
+ private CancellationTokenSource? _cancellationTokenSource;
+ private bool _isRunning = false;
+ private readonly object _queueLock = new object();
+
+ // Konfigurierbare Parameter
+ private const int UploadRetryDelayMs = 5000; // 5 Sekunden Wartezeit zwischen Retries
+ private const int MaxRetries = 3;
+ private const int CheckIntervalMs = 2000; // Überprüfe Queue alle 2 Sekunden
+
+ public UploadQueueService(Logger logger, AppSettingsService appSettings, LycheeUploadService lycheeUploadService)
+ {
+ _logger = logger;
+ _appSettings = appSettings;
+ _lycheeUploadService = lycheeUploadService;
+ _uploadTracker = new UploadTracker(logger, "UploadTracking");
+ _uploadQueue = new Queue();
+ }
+
+ ///
+ /// Startet den Upload-Service
+ ///
+ public void Start()
+ {
+ if (_isRunning)
+ {
+ _logger.Warning("Upload-Service läuft bereits");
+ return;
+ }
+
+ _isRunning = true;
+ _cancellationTokenSource = new CancellationTokenSource();
+ _uploadTask = ProcessQueueAsync(_cancellationTokenSource.Token);
+ _logger.Info("Upload-Service gestartet");
+ }
+
+ ///
+ /// Stoppt den Upload-Service
+ ///
+ public async Task StopAsync()
+ {
+ if (!_isRunning)
+ {
+ return;
+ }
+
+ _isRunning = false;
+ _cancellationTokenSource?.Cancel();
+
+ if (_uploadTask != null)
+ {
+ try
+ {
+ await _uploadTask;
+ }
+ catch (OperationCanceledException)
+ {
+ // Erwartet
+ }
+ }
+
+ _logger.Info("Upload-Service gestoppt");
+ }
+
+ ///
+ /// Scannt das Bild-Verzeichnis und fügt fehlgeschlagene Uploads zur Queue hinzu
+ ///
+ public void ScanAndQueueFailedUploads()
+ {
+ try
+ {
+ if (!_appSettings.LycheeAutoUploadEnabled)
+ {
+ _logger.Debug("Lychee Auto-Upload ist deaktiviert");
+ return;
+ }
+
+ var failedFiles = _uploadTracker.GetFailedUploads(_appSettings.PictureLocation);
+
+ lock (_queueLock)
+ {
+ foreach (var filePath in failedFiles)
+ {
+ if (!_uploadQueue.Contains(filePath))
+ {
+ _uploadQueue.Enqueue(filePath);
+ _logger.Debug($"Fehlgeschlagenes Upload in Queue hinzugefügt: {Path.GetFileName(filePath)}");
+ }
+ }
+ }
+
+ if (failedFiles.Count > 0)
+ {
+ _logger.Info($"{failedFiles.Count} fehlgeschlagene Uploads in Queue hinzugefügt");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Fehler beim Scannen fehlgeschlagener Uploads: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Fügt ein neu erstelltes Foto zur Upload-Queue hinzu
+ ///
+ public void QueueNewPhoto(string photoPath)
+ {
+ if (!_appSettings.LycheeAutoUploadEnabled)
+ {
+ _logger.Debug("Lychee Auto-Upload ist deaktiviert");
+ return;
+ }
+
+ lock (_queueLock)
+ {
+ if (!_uploadQueue.Contains(photoPath))
+ {
+ _uploadQueue.Enqueue(photoPath);
+ _logger.Info($"Neues Foto in Upload-Queue hinzugefügt: {Path.GetFileName(photoPath)}");
+ }
+ }
+ }
+
+ ///
+ /// Gibt die aktuelle Größe der Upload-Queue zurück
+ ///
+ public int GetQueueSize()
+ {
+ lock (_queueLock)
+ {
+ return _uploadQueue.Count;
+ }
+ }
+
+ ///
+ /// Hauptschleife für die Verarbeitung der Upload-Queue
+ ///
+ private async Task ProcessQueueAsync(CancellationToken cancellationToken)
+ {
+ _logger.Info("Upload-Queue-Verarbeitung gestartet");
+
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ string? filePath = null;
+
+ lock (_queueLock)
+ {
+ if (_uploadQueue.Count > 0)
+ {
+ filePath = _uploadQueue.Dequeue();
+ }
+ }
+
+ if (filePath != null)
+ {
+ await ProcessUploadAsync(filePath, cancellationToken);
+ }
+ else
+ {
+ // Kurz warten, wenn Queue leer
+ await Task.Delay(CheckIntervalMs, cancellationToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Fehler in Upload-Queue-Verarbeitung: {ex.Message}");
+ await Task.Delay(UploadRetryDelayMs, cancellationToken);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.Info("Upload-Queue-Verarbeitung wurde abgebrochen");
+ }
+ }
+
+ ///
+ /// Verarbeitet den Upload einer einzelnen Datei mit Retry-Logik
+ ///
+ private async Task ProcessUploadAsync(string filePath, CancellationToken cancellationToken)
+ {
+ var fileName = Path.GetFileName(filePath);
+
+ try
+ {
+ // Prüfe, ob Datei noch existiert
+ if (!File.Exists(filePath))
+ {
+ _logger.Warning($"Bild nicht gefunden, überspringe: {fileName}");
+ _uploadTracker.MarkAsFailedUpload(fileName, "Datei nicht gefunden");
+ return;
+ }
+
+ // Prüfe, ob bereits hochgeladen
+ if (_uploadTracker.IsUploaded(filePath))
+ {
+ _logger.Debug($"Bild bereits hochgeladen: {fileName}");
+ return;
+ }
+
+ // Versuche Upload mit Retry
+ bool uploadSuccess = false;
+ string? lastError = null;
+
+ for (int attempt = 1; attempt <= MaxRetries; attempt++)
+ {
+ try
+ {
+ _logger.Info($"Upload-Versuch {attempt}/{MaxRetries} für {fileName}...");
+
+ var albumId = _appSettings.LycheeDefaultAlbumId;
+ uploadSuccess = await _lycheeUploadService.UploadImageAsync(filePath, albumId);
+
+ if (uploadSuccess)
+ {
+ _uploadTracker.MarkAsUploaded(filePath);
+ _logger.Info($"✅ Upload erfolgreich: {fileName}");
+ return;
+ }
+ else
+ {
+ lastError = "Upload-Fehler vom Service";
+ _logger.Warning($"Upload fehlgeschlagen (Versuch {attempt}): {fileName}");
+ }
+ }
+ catch (Exception ex)
+ {
+ lastError = ex.Message;
+ _logger.Error($"Fehler bei Upload-Versuch {attempt}: {ex.Message}");
+ }
+
+ // Warte vor nächstem Versuch (nicht nach dem letzten)
+ if (attempt < MaxRetries)
+ {
+ await Task.Delay(UploadRetryDelayMs, cancellationToken);
+ }
+ }
+
+ // Alle Versuche fehlgeschlagen
+ _uploadTracker.MarkAsFailedUpload(filePath, lastError ?? "Unbekannter Fehler");
+ _logger.Error($"❌ Upload fehlgeschlagen nach {MaxRetries} Versuchen: {fileName}");
+
+ // Füge wieder zur Queue hinzu für späteren Retry (längerer Abstand)
+ _logger.Info($"Bild wird später erneut versucht: {fileName}");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Kritischer Fehler bei Upload-Verarbeitung von {fileName}: {ex.Message}");
+ _uploadTracker.MarkAsFailedUpload(filePath, ex.Message);
+ }
+ }
+
+ public void Dispose()
+ {
+ StopAsync().Wait(TimeSpan.FromSeconds(5));
+ _cancellationTokenSource?.Dispose();
+ _lycheeUploadService?.Dispose();
+ }
+}
diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/UploadTracker.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/UploadTracker.cs
new file mode 100644
index 0000000..bbc6cd3
--- /dev/null
+++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/UploadTracker.cs
@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using CamBooth.App.Core.Logging;
+
+namespace CamBooth.App.Features.LycheeUpload;
+
+///
+/// Verfolgt welche Bilder bereits hochgeladen wurden
+///
+public class UploadTracker
+{
+ private readonly Logger _logger;
+ private readonly string _trackerFilePath;
+ private Dictionary _uploadedImages;
+
+ public class UploadedImageInfo
+ {
+ public string FileName { get; set; }
+ public DateTime UploadedAt { get; set; }
+ public string? LycheeId { get; set; }
+ public int RetryCount { get; set; }
+ public DateTime? LastRetryAt { get; set; }
+ public string? ErrorMessage { get; set; }
+ }
+
+ public UploadTracker(Logger logger, string trackerDirectory = "")
+ {
+ _logger = logger;
+
+ // Verwende absoluten Pfad
+ if (string.IsNullOrWhiteSpace(trackerDirectory))
+ {
+ trackerDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UploadTracking");
+ }
+ else if (!Path.IsPathRooted(trackerDirectory))
+ {
+ trackerDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, trackerDirectory);
+ }
+
+ if (!Directory.Exists(trackerDirectory))
+ {
+ Directory.CreateDirectory(trackerDirectory);
+ }
+
+ _trackerFilePath = Path.Combine(trackerDirectory, "upload_history.json");
+ _uploadedImages = new Dictionary();
+
+ LoadFromDisk();
+ }
+
+ ///
+ /// Lädt den Upload-History aus der Datei
+ ///
+ private void LoadFromDisk()
+ {
+ try
+ {
+ if (File.Exists(_trackerFilePath))
+ {
+ var json = File.ReadAllText(_trackerFilePath);
+ var data = JsonSerializer.Deserialize>(json);
+ _uploadedImages = data ?? new Dictionary();
+ _logger.Info($"Upload-History geladen: {_uploadedImages.Count} Einträge");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Fehler beim Laden der Upload-History: {ex.Message}");
+ _uploadedImages = new Dictionary();
+ }
+ }
+
+ ///
+ /// Speichert den Upload-History auf die Festplatte
+ ///
+ private void SaveToDisk()
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(_uploadedImages, new JsonSerializerOptions { WriteIndented = true });
+ File.WriteAllText(_trackerFilePath, json);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Fehler beim Speichern der Upload-History: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Markiert ein Bild als erfolgreich hochgeladen
+ ///
+ public void MarkAsUploaded(string fileName, string? lycheeId = null)
+ {
+ var fileNameOnly = Path.GetFileName(fileName);
+
+ _uploadedImages[fileNameOnly] = new UploadedImageInfo
+ {
+ FileName = fileNameOnly,
+ UploadedAt = DateTime.UtcNow,
+ LycheeId = lycheeId,
+ RetryCount = 0,
+ LastRetryAt = null,
+ ErrorMessage = null
+ };
+
+ SaveToDisk();
+ _logger.Info($"Bild als hochgeladen markiert: {fileNameOnly}");
+ }
+
+ ///
+ /// Markiert einen Upload-Fehler für ein Bild
+ ///
+ public void MarkAsFailedUpload(string fileName, string? errorMessage = null)
+ {
+ var fileNameOnly = Path.GetFileName(fileName);
+
+ if (_uploadedImages.ContainsKey(fileNameOnly))
+ {
+ var info = _uploadedImages[fileNameOnly];
+ info.RetryCount++;
+ info.LastRetryAt = DateTime.UtcNow;
+ info.ErrorMessage = errorMessage;
+ }
+ else
+ {
+ _uploadedImages[fileNameOnly] = new UploadedImageInfo
+ {
+ FileName = fileNameOnly,
+ UploadedAt = DateTime.UtcNow,
+ RetryCount = 1,
+ LastRetryAt = DateTime.UtcNow,
+ ErrorMessage = errorMessage
+ };
+ }
+
+ SaveToDisk();
+ _logger.Warning($"Upload-Fehler für {fileNameOnly}: {errorMessage}");
+ }
+
+ ///
+ /// Prüft, ob ein Bild bereits hochgeladen wurde
+ ///
+ public bool IsUploaded(string fileName)
+ {
+ var fileNameOnly = Path.GetFileName(fileName);
+ return _uploadedImages.ContainsKey(fileNameOnly) && _uploadedImages[fileNameOnly].UploadedAt != default;
+ }
+
+ ///
+ /// Gibt alle Bilder zurück, die noch nicht hochgeladen wurden
+ ///
+ public List GetFailedUploads(string pictureDirectory)
+ {
+ var failedFiles = new List();
+
+ if (!Directory.Exists(pictureDirectory))
+ {
+ return failedFiles;
+ }
+
+ try
+ {
+ var allFiles = Directory.GetFiles(pictureDirectory, "img_*.jpg");
+
+ foreach (var filePath in allFiles)
+ {
+ var fileName = Path.GetFileName(filePath);
+
+ // Wenn nicht im Tracker oder mit Fehler → zu Retry-Liste hinzufügen
+ if (!_uploadedImages.ContainsKey(fileName) ||
+ _uploadedImages[fileName].UploadedAt == default)
+ {
+ failedFiles.Add(filePath);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Fehler beim Scannen von {pictureDirectory}: {ex.Message}");
+ }
+
+ return failedFiles;
+ }
+
+ ///
+ /// Gibt die Anzahl fehlgeschlagener Uploads zurück
+ ///
+ public int GetFailedUploadCount()
+ {
+ return _uploadedImages.Count(x => x.Value.UploadedAt == default);
+ }
+}
diff --git a/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml b/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml
index 15cf199..1817be5 100644
--- a/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml
+++ b/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml
@@ -14,6 +14,19 @@
+
+
+
@@ -36,15 +49,17 @@
-
-
@@ -54,15 +69,17 @@
Text="Seite 1 von 1" MinWidth="150" TextAlignment="Center" />
-
-
diff --git a/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml.cs b/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml.cs
index 0c1befb..eb60cca 100644
--- a/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml.cs
+++ b/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml.cs
@@ -1,4 +1,4 @@
-using System.Windows;
+using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
@@ -7,6 +7,7 @@ using System.Windows.Media.Imaging;
using CamBooth.App.Core.AppSettings;
using CamBooth.App.Core.Logging;
+using CamBooth.App.Features.LycheeUpload;
using Wpf.Ui.Controls;
@@ -24,18 +25,24 @@ public partial class PictureGalleryPage : Page
private readonly PictureGalleryService _pictureGalleryService;
+ private readonly LycheeUploadService _lycheeUploadService;
+
private ContentDialog? _openContentDialog;
private int _currentPage = 1;
- private int _itemsPerPage = 12;
+ private int _itemsPerPage = 11;
private int _totalPages = 1;
+ // QR Code wird nur einmal erstellt und dann wiederverwendet
+ private Border? _qrCodeBorder = null;
- public PictureGalleryPage(AppSettingsService appSettingsService, Logger logger, PictureGalleryService pictureGalleryService)
+
+ public PictureGalleryPage(AppSettingsService appSettingsService, Logger logger, PictureGalleryService pictureGalleryService, LycheeUploadService lycheeUploadService)
{
this._appSettingsService = appSettingsService;
this._logger = logger;
this._pictureGalleryService = pictureGalleryService;
+ this._lycheeUploadService = lycheeUploadService;
this.InitializeComponent();
this.Initialize();
}
@@ -45,6 +52,9 @@ public partial class PictureGalleryPage : Page
{
try
{
+ // QR-Code nur einmal beim Initialisieren erstellen
+ CreateQRCodeBorder();
+
_currentPage = 1;
CalculateTotalPages();
LoadCurrentPage();
@@ -56,6 +66,56 @@ public partial class PictureGalleryPage : Page
}
}
+ private void CreateQRCodeBorder()
+ {
+ try
+ {
+ var qrCodeImage = this._lycheeUploadService.GenerateGalleryQRCode();
+ if (qrCodeImage != null)
+ {
+ // QR-Code Container mit Label
+ var qrContainer = new StackPanel { Orientation = Orientation.Vertical };
+
+ var qrImageControl = new Image
+ {
+ Source = qrCodeImage,
+ Width = 220,
+ Margin = new Thickness(4)
+ };
+
+ var qrSubLabel = new TextBlock
+ {
+ Text = "zum Fotoalbum",
+ Foreground = new SolidColorBrush(Colors.White),
+ FontSize = 11,
+ FontWeight = FontWeights.Bold,
+ TextAlignment = TextAlignment.Center,
+ Margin = new Thickness(4, 0, 4, 2)
+ };
+
+ qrContainer.Children.Add(qrSubLabel);
+ qrContainer.Children.Add(qrImageControl);
+
+ _qrCodeBorder = new Border
+ {
+ Background = new SolidColorBrush(Colors.Black),
+ BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D4AF37")),
+ BorderThickness = new Thickness(2),
+ CornerRadius = new CornerRadius(6),
+ Padding = new Thickness(4),
+ Child = qrContainer,
+ Margin = new Thickness(4)
+ };
+
+ this._logger.Debug("✅ QR-Code statisch erstellt");
+ }
+ }
+ catch (Exception ex)
+ {
+ this._logger.Warning($"QR-Code konnte nicht generiert werden: {ex.Message}");
+ }
+ }
+
private void ContentDialog_OnButtonClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
@@ -74,6 +134,13 @@ public partial class PictureGalleryPage : Page
// Clear existing items
this.PicturesPanel.Items.Clear();
+ // Füge den statischen QR-Code als erstes Element hinzu
+ if (_qrCodeBorder != null)
+ {
+ this.PicturesPanel.Items.Add(_qrCodeBorder);
+ }
+
+ // Jetzt die regulären Bilder hinzufügen
int totalThumbnails = this._pictureGalleryService.ThumbnailsOrderedByNewestDescending.Count;
int endIndex = Math.Min(startIndex + count, totalThumbnails);
@@ -276,6 +343,19 @@ public partial class PictureGalleryPage : Page
UpdatePagerControls();
GalleryScrollViewer.ScrollToTop();
}
+
+ private void CloseGallery_Click(object sender, RoutedEventArgs e)
+ {
+ // Finde das MainWindow über die Window.GetWindow Methode
+ var window = Window.GetWindow(this);
+ if (window is MainWindow mainWindow)
+ {
+ mainWindow.ClosePicturePanel();
+ }
+ else
+ {
+ _logger.Warning("MainWindow konnte nicht gefunden werden, um die Galerie zu schließen");
+ }
+ }
}
-
diff --git a/src/CamBooth/CamBooth.App/MainWindow.xaml b/src/CamBooth/CamBooth.App/MainWindow.xaml
index 36db703..7686fb9 100644
--- a/src/CamBooth/CamBooth.App/MainWindow.xaml
+++ b/src/CamBooth/CamBooth.App/MainWindow.xaml
@@ -53,28 +53,245 @@
Margin="24 24 24 12"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -205,11 +387,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Panel.ZIndex="10"
+ Visibility="Collapsed">
@@ -112,6 +117,114 @@ public partial class MainWindow : Window
logger.Info("MainWindow initialized");
}
+ private async Task InitializeAsync()
+ {
+ try
+ {
+ // Zeige Ladeanzeige
+ this.LoadingOverlay.Visibility = Visibility.Visible;
+ this.WelcomeOverlay.Visibility = Visibility.Collapsed;
+
+ // Lade Thumbnails mit Progress-Updates
+ await LoadThumbnailsWithProgress();
+
+ // Warte kurz, damit der Benutzer die Fertigstellung sehen kann
+ await Task.Delay(500);
+
+ // Verstecke Ladeanzeige und zeige Welcome Screen
+ this.Dispatcher.Invoke(() =>
+ {
+ this.LoadingOverlay.Visibility = Visibility.Collapsed;
+ this.WelcomeOverlay.Visibility = Visibility.Visible;
+ });
+
+ this._logger.Info("Initialization completed successfully");
+ }
+ catch (Exception ex)
+ {
+ this._logger.Error($"Initialization error: {ex.Message}");
+ // Bei Fehler trotzdem zum Welcome Screen wechseln
+ this.Dispatcher.Invoke(() =>
+ {
+ this.LoadingOverlay.Visibility = Visibility.Collapsed;
+ this.WelcomeOverlay.Visibility = Visibility.Visible;
+ });
+ }
+ }
+
+ private async Task LoadThumbnailsWithProgress()
+ {
+ string pictureLocation = this._appSettings.PictureLocation;
+
+ // Sicherstellen, dass das Verzeichnis existiert
+ if (!Directory.Exists(pictureLocation))
+ {
+ this._logger.Info($"Picture directory does not exist: '{pictureLocation}'. Creating it...");
+ try
+ {
+ Directory.CreateDirectory(pictureLocation);
+ this._logger.Info($"Picture directory created: '{pictureLocation}'");
+
+ this.Dispatcher.Invoke(() =>
+ {
+ this.LoadingStatusText.Text = "Keine Fotos gefunden";
+ this.LoadingCountText.Text = "0 Fotos";
+ });
+
+ await Task.Delay(1000);
+ return;
+ }
+ catch (Exception ex)
+ {
+ this._logger.Error($"Failed to create picture directory: {ex.Message}");
+ this.Dispatcher.Invoke(() =>
+ {
+ this.LoadingStatusText.Text = "Fehler beim Erstellen des Foto-Ordners";
+ this.LoadingCountText.Text = "";
+ });
+ await Task.Delay(2000);
+ return;
+ }
+ }
+
+ // Zähle Bilddateien
+ string[] imageExtensions = { ".jpg", ".jpeg", ".png", ".bmp", ".gif" };
+ var picturePaths = Directory.EnumerateFiles(pictureLocation)
+ .Where(f => imageExtensions.Contains(Path.GetExtension(f).ToLower()))
+ .ToList();
+
+ int totalCount = picturePaths.Count;
+
+ if (totalCount == 0)
+ {
+ this._logger.Info($"No pictures found in directory: '{pictureLocation}'");
+ this.Dispatcher.Invoke(() =>
+ {
+ this.LoadingStatusText.Text = "Keine Fotos gefunden";
+ this.LoadingCountText.Text = "Bereit für neue Aufnahmen!";
+ });
+ await Task.Delay(1000);
+ return;
+ }
+
+ // Update UI mit Gesamtanzahl
+ this.Dispatcher.Invoke(() =>
+ {
+ this.LoadingStatusText.Text = $"Lade {totalCount} Foto{(totalCount != 1 ? "s" : "")}...";
+ this.LoadingCountText.Text = $"0 / {totalCount}";
+ });
+
+ // Lade Thumbnails
+ await this._pictureGalleryService.LoadThumbnailsToCache();
+
+ // Update UI nach dem Laden
+ this.Dispatcher.Invoke(() =>
+ {
+ this.LoadingStatusText.Text = "Fotos erfolgreich geladen!";
+ this.LoadingCountText.Text = $"{totalCount} Foto{(totalCount != 1 ? "s" : "")} bereit";
+ });
+ }
+
private void TimerControlRectangleAnimation_OnTimerEllapsed()
{
@@ -147,20 +260,28 @@ public partial class MainWindow : Window
if (visibility)
{
this.HideGalleryPrompt();
- this.PicturePanel.Navigate(new PictureGalleryPage(this._appSettings, this._logger, this._pictureGalleryService));
+ this.PicturePanel.Navigate(new PictureGalleryPage(this._appSettings, this._logger, this._pictureGalleryService, this._lycheeUploadService));
// Reset new photo count when opening gallery
this._pictureGalleryService.ResetNewPhotoCount();
+ // Blende unnötige Buttons aus, wenn Galerie geöffnet wird
+ this.ButtonPanel.Visibility = Visibility.Hidden;
+ this.ActionButtonsContainer.Visibility = Visibility.Hidden;
+ this.ShutdownDock.Visibility = Visibility.Hidden;
}
else
{
this.PicturePanel.ClearValue(MainWindow.ContentProperty);
+ // Stelle Buttons wieder her, wenn Galerie geschlossen wird
+ this.ButtonPanel.Visibility = Visibility.Visible;
+ this.ActionButtonsContainer.Visibility = Visibility.Visible;
+ this.ShutdownDock.Visibility = Visibility.Visible;
}
this._isPicturePanelVisible = !visibility;
}
- private void ClosePicturePanel()
+ public void ClosePicturePanel()
{
if (this.PicturePanel.Content is PictureGalleryPage pictureGalleryPage)
{
@@ -187,7 +308,7 @@ public partial class MainWindow : Window
this.StartLiveViewIfNeeded();
this.WelcomeOverlay.Visibility = Visibility.Collapsed;
this.ButtonPanel.Visibility = Visibility.Visible;
- this.PictureGalleryDock.Visibility = Visibility.Visible;
+ this.ActionButtonsContainer.Visibility = Visibility.Visible;
this.ShutdownDock.Visibility = Visibility.Visible;
}
@@ -284,11 +405,11 @@ public partial class MainWindow : Window
private void SwitchButtonAndTimerPanel()
{
this.ButtonPanel.Visibility = this.ButtonPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
- this.PictureGalleryDock.Visibility = this.PictureGalleryDock.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
+ this.ActionButtonsContainer.Visibility = this.ActionButtonsContainer.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
this.TimerPanel.Visibility = this.TimerPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
}
- private void SetVisibilityPicturePanel(object sender, RoutedEventArgs e)
+ private void SetVisibilityPicturePanel(object sender, MouseButtonEventArgs e)
{
this.SetVisibilityPicturePanel(this._isPicturePanelVisible);
}
@@ -339,8 +460,24 @@ public partial class MainWindow : Window
System.Windows.MessageBox.Show("Windows konnte nicht heruntergefahren werden. Bitte erneut versuchen.");
}
}
-
- this.Close();
+ else
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = "shutdown",
+ Arguments = "/l", // = logoff (abmelden)
+ CreateNoWindow = true,
+ UseShellExecute = false
+ });
+ }
+ catch (Exception exception)
+ {
+ this._logger.Error(exception.Message);
+ System.Windows.MessageBox.Show("Abmeldung fehlgeschlagen");
+ }
+ }
}
private void ToggleShutdownSlider(object sender, RoutedEventArgs e)
@@ -454,4 +591,33 @@ public partial class MainWindow : Window
this.NewPhotosBadge.Visibility = Visibility.Collapsed;
}
}
+
+ private void ShowQRCode(object sender, MouseButtonEventArgs e)
+ {
+ try
+ {
+ this._logger.Info("Zeige QR-Code an...");
+
+ // Generiere QR-Code
+ var qrCodeImage = this._lycheeUploadService.GenerateGalleryQRCode();
+
+ if (qrCodeImage == null)
+ {
+ System.Windows.MessageBox.Show("QR-Code konnte nicht generiert werden. Bitte überprüfe die Lychee-Konfiguration.",
+ "Fehler", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
+ return;
+ }
+
+ // Öffne QR-Code-Fenster
+ var qrWindow = new QRCodeDisplayWindow();
+ qrWindow.SetQRCode(qrCodeImage);
+ qrWindow.ShowDialog();
+ }
+ catch (Exception ex)
+ {
+ this._logger.Error($"Fehler beim Anzeigen des QR-Codes: {ex.Message}");
+ System.Windows.MessageBox.Show($"Fehler beim Anzeigen des QR-Codes: {ex.Message}",
+ "Fehler", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
+ }
+ }
}
diff --git a/src/CamBooth/CamBooth.App/Resources/ButtonStyles.xaml b/src/CamBooth/CamBooth.App/Resources/ButtonStyles.xaml
index b67e357..97db590 100644
--- a/src/CamBooth/CamBooth.App/Resources/ButtonStyles.xaml
+++ b/src/CamBooth/CamBooth.App/Resources/ButtonStyles.xaml
@@ -1,115 +1,263 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CamBooth/SESSION_NOT_OPEN_FIX.md b/src/CamBooth/SESSION_NOT_OPEN_FIX.md
new file mode 100644
index 0000000..204701d
--- /dev/null
+++ b/src/CamBooth/SESSION_NOT_OPEN_FIX.md
@@ -0,0 +1,172 @@
+# SESSION_NOT_OPEN Error - Root Cause and Fix
+
+## Problem Summary
+The application was failing with `SESSION_NOT_OPEN` error when trying to start the live view camera feed:
+```
+Failed to start live view: SESSION_NOT_OPEN at EOSDigital.API.STAThread.Invoke(Action action)
+at EOSDigital.API.Camera.OpenSession()
+at CamBooth.App.Features.Camera.CameraService.OpenSession()
+```
+
+## Root Cause Analysis
+
+### Issue 1: Uninitialized Camera Instance at Startup
+In `App.xaml.cs`, the `ICamera` dependency is registered as a **Singleton at application startup**:
+```csharp
+services.AddSingleton();
+```
+
+This creates a single Camera instance **before any actual physical camera is connected**, and this uninitialized instance is never properly updated when cameras are detected later.
+
+### Issue 2: Stale Camera Reference
+The `CameraService` constructor receives this uninitialized `_mainCamera` instance:
+```csharp
+public CameraService(..., ICamera mainCamera, ICanonAPI APIHandler)
+{
+ this._mainCamera = mainCamera; // This is the uninitialized singleton
+ ...
+}
+```
+
+### Issue 3: Incorrect Flow in ConnectCamera()
+The original `ConnectCamera()` method called `OpenSession()` **before reassigning** `_mainCamera`:
+```csharp
+public void ConnectCamera()
+{
+ this.RefreshCamera(); // Gets camera list
+ List cameraList = this._APIHandler.GetCameraList();
+ if (cameraList.Any())
+ {
+ this.OpenSession(); // ← Uses OLD _mainCamera, not the newly detected one!
+ // ...
+ }
+}
+```
+
+The `OpenSession()` method did try to reassign `_mainCamera`, but the error was thrown **before reaching that point** in some error conditions.
+
+## The Fix
+
+### Change 1: Update ConnectCamera() Method
+Moved the `_mainCamera` assignment **before** calling `OpenSession()`:
+
+```csharp
+public void ConnectCamera()
+{
+ ErrorHandler.SevereErrorHappened += this.ErrorHandler_SevereErrorHappened;
+ ErrorHandler.NonSevereErrorHappened += this.ErrorHandler_NonSevereErrorHappened;
+
+ try
+ {
+ this.RefreshCamera();
+
+ // Retry logic for camera detection (some systems need time to initialize)
+ int maxRetries = 3;
+ int retryDelay = 500; // milliseconds
+
+ for (int attempt = 0; attempt < maxRetries; attempt++)
+ {
+ if (this.CamList != null && this.CamList.Any())
+ {
+ break;
+ }
+
+ if (attempt < maxRetries - 1)
+ {
+ this._logger.Info($"No cameras found on attempt {attempt + 1}/{maxRetries}, retrying in {retryDelay}ms...");
+ System.Threading.Thread.Sleep(retryDelay);
+ this.RefreshCamera();
+ }
+ }
+
+ if (this.CamList == null || !this.CamList.Any())
+ {
+ this.ReportError("No cameras / devices found");
+ throw new InvalidOperationException("No cameras / devices found after multiple attempts");
+ }
+
+ this._logger.Info($"Found {this.CamList.Count} camera(s)");
+ string cameraDeviceNames = string.Join(", ", this.CamList.Select(cam => cam.DeviceName));
+ this._logger.Info(cameraDeviceNames);
+
+ // ← UPDATE _mainCamera BEFORE calling OpenSession()
+ this._mainCamera = this.CamList[0];
+ this._logger.Info($"Selected camera: {this._mainCamera.DeviceName}");
+
+ this.OpenSession();
+ this.SetSettingSaveToComputer();
+ this.StarLiveView();
+ }
+ catch (Exception ex)
+ {
+ this._logger.Error($"Error connecting camera: {ex.Message}");
+ throw;
+ }
+}
+```
+
+**Key improvements:**
+1. **Explicit camera assignment before OpenSession()** - Ensures the correct physical camera is used
+2. **Retry logic** - Handles SDK initialization delays on some systems
+3. **Better error messages** - Clearer logging of the connection process
+
+### Change 2: Simplify OpenSession() Method
+Removed redundant camera reassignment and simplified validation:
+
+```csharp
+private void OpenSession()
+{
+ try
+ {
+ if (this._mainCamera == null)
+ {
+ throw new InvalidOperationException("Camera reference is null. Make sure ConnectCamera is called first.");
+ }
+
+ // Check if session is already open
+ if (this._mainCamera.SessionOpen)
+ {
+ this._logger.Info($"Camera session already open for {this._mainCamera.DeviceName}");
+ return;
+ }
+
+ this._logger.Info($"Opening session for camera: {this._mainCamera.DeviceName}");
+ this._mainCamera.OpenSession();
+ this._logger.Info("Camera session opened successfully");
+
+ this._mainCamera.StateChanged += this.MainCamera_StateChanged;
+ this._mainCamera.DownloadReady += this.MainCamera_DownloadReady;
+
+ this.AvList = this._mainCamera.GetSettingsList(PropertyID.Av);
+ this.TvList = this._mainCamera.GetSettingsList(PropertyID.Tv);
+ this.ISOList = this._mainCamera.GetSettingsList(PropertyID.ISO);
+ }
+ catch (Exception ex)
+ {
+ this._logger.Error($"Failed to open camera session: {ex.Message}");
+ this.ReportError($"Failed to open camera session: {ex.Message}");
+ throw;
+ }
+}
+```
+
+**Improvements:**
+1. **No redundant camera assignment** - Camera is already set by ConnectCamera()
+2. **Clearer null check** - Validates that ConnectCamera() was called first
+3. **Maintained duplex open check** - Still checks if session is already open
+
+## Files Modified
+- `CamBooth.App\Features\Camera\CameraService.cs`
+ - `ConnectCamera()` method
+ - `OpenSession()` method
+
+## Testing Recommendations
+1. **Test with camera connected** - Verify live view starts successfully
+2. **Test with camera disconnected** - Verify graceful error handling with retries
+3. **Test with delayed camera detection** - Ensure retry logic gives SDK time to initialize
+4. **Test camera reconnection** - Disconnect and reconnect camera to verify session management
+
+## Additional Notes
+- The singleton pattern for `ICamera` in dependency injection is not ideal, but this fix ensures the correct instance is used once a physical camera is detected
+- The retry logic (3 attempts, 500ms delay) can be adjusted based on system requirements
+- Consider future refactoring to use factory pattern for camera initialization instead of singleton