Refactor: Simplify CameraService, improve session handling, and streamline application initialization

This commit is contained in:
iTob 2026-03-09 22:24:39 +01:00
parent 91935cd41c
commit b3c91da331
5 changed files with 975 additions and 1242 deletions

View File

@ -1,4 +1,3 @@
using System.IO;
using System.Windows; using System.Windows;
using CamBooth.App.Core.AppSettings; using CamBooth.App.Core.AppSettings;
@ -21,134 +20,113 @@ namespace CamBooth.App;
/// </summary> /// </summary>
public partial class App : Application public partial class App : Application
{ {
private IServiceProvider _serviceProvider; private IServiceProvider? _serviceProvider;
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
base.OnStartup(e); base.OnStartup(e);
// Konfiguration laden var configuration = BuildConfiguration();
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var configBuilder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("Core/AppSettings/app.settings.json", optional: false, reloadOnChange: true);
if (environment == "Development")
{
configBuilder.AddJsonFile("Core/AppSettings/app.settings.dev.json", optional: true, reloadOnChange: true);
}
var configuration = configBuilder.Build();
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration); RegisterServices(services, configuration);
services.AddSingleton<Logger>();
services.AddSingleton<AppSettingsService>();
services.AddSingleton<PictureGalleryService>();
services.AddSingleton<CameraService>();
services.AddSingleton<PhotoPrismAuthService>();
services.AddSingleton<PhotoPrismUploadService>();
services.AddSingleton<PhotoPrismUploadQueueService>();
var tempProvider = services.BuildServiceProvider();
var appSettings = tempProvider.GetRequiredService<AppSettingsService>();
var logger = tempProvider.GetRequiredService<Logger>();
try
{
if (!Directory.Exists(appSettings.PictureLocation))
{
Directory.CreateDirectory(appSettings.PictureLocation);
logger.Debug($"Picture directory created: {appSettings.PictureLocation}");
}
}
catch (Exception ex)
{
logger.Error($"Failed to create picture directory: {ex.Message}");
}
// Jetzt die Camera Services basierend auf AppSettings registrieren
// Mit Try-Catch für fehlende DLL-Abhängigkeiten
try
{
if (appSettings.UseMockCamera)
{
services.AddSingleton<ICanonAPI, CanonAPIMock>();
}
else
{
services.AddSingleton<ICanonAPI, CanonAPI>();
}
}
catch (DllNotFoundException ex)
{
// Falls EDSDK DLL nicht gefunden, fallback auf Mock
MessageBox.Show(
$"EDSDK konnte nicht geladen werden. Verwende Mock-Kamera.\n\nFehler: {ex.Message}",
"DLL nicht gefunden",
MessageBoxButton.OK,
MessageBoxImage.Warning);
services.AddSingleton<ICanonAPI, CanonAPIMock>();
}
services.AddTransient<MainWindow>();
_serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();
// Starte PhotoPrism Upload-Service beim Start StartBackgroundServices();
try
{
var uploadQueueService = _serviceProvider.GetRequiredService<PhotoPrismUploadQueueService>();
uploadQueueService.Start();
// Scan für fehlgeschlagene Uploads beim Start _serviceProvider.GetRequiredService<MainWindow>().Show();
uploadQueueService.ScanAndQueueFailedUploads();
logger.Info("PhotoPrism UploadQueueService initialisiert und gestartet");
}
catch (Exception ex)
{
logger.Error($"Fehler beim Start des PhotoPrism UploadQueueService: {ex.Message}");
}
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
} }
protected override void OnExit(ExitEventArgs e) protected override void OnExit(ExitEventArgs e)
{ {
// Stoppe PhotoPrism UploadQueueService beim Beenden der App
try try
{ {
var uploadQueueService = _serviceProvider?.GetService<PhotoPrismUploadQueueService>(); _serviceProvider?.GetService<PhotoPrismUploadQueueService>()
if (uploadQueueService != null) ?.StopAsync().Wait(TimeSpan.FromSeconds(10));
{
uploadQueueService.StopAsync().Wait(TimeSpan.FromSeconds(10));
}
} }
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine($"Fehler beim Stoppen des PhotoPrism UploadQueueService: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"Error stopping upload service: {ex.Message}");
} }
// Dispose Service Provider, damit IDisposable-Services (z.B. CameraService) sauber beendet werden.
try try
{ {
if (_serviceProvider is IDisposable disposableProvider) (_serviceProvider as IDisposable)?.Dispose();
{
disposableProvider.Dispose();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine($"Fehler beim Dispose des ServiceProviders: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"Error disposing service provider: {ex.Message}");
} }
base.OnExit(e); base.OnExit(e);
} }
private static IConfiguration BuildConfiguration()
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var builder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("Core/AppSettings/app.settings.json", optional: false, reloadOnChange: true);
if (environment == "Development")
builder.AddJsonFile("Core/AppSettings/app.settings.dev.json", optional: true, reloadOnChange: true);
return builder.Build();
}
private static void RegisterServices(ServiceCollection services, IConfiguration configuration)
{
services.AddSingleton(configuration);
services.AddSingleton<Logger>();
services.AddSingleton<AppSettingsService>();
services.AddSingleton<PictureGalleryService>();
services.AddSingleton<CameraService>();
services.AddSingleton<PhotoPrismAuthService>();
services.AddSingleton<PhotoPrismUploadService>();
services.AddSingleton<PhotoPrismUploadQueueService>();
services.AddSingleton<MainWindowViewModel>();
services.AddTransient<MainWindow>();
RegisterCameraApi(services, configuration);
}
private static void RegisterCameraApi(ServiceCollection services, IConfiguration configuration)
{
var useMockCamera = bool.Parse(configuration["AppSettings:UseMockCamera"] ?? "false");
try
{
ICanonAPI canonApi = useMockCamera ? new CanonAPIMock() : new CanonAPI();
services.AddSingleton(canonApi);
}
catch (DllNotFoundException ex)
{
MessageBox.Show(
$"EDSDK konnte nicht geladen werden. Verwende Mock-Kamera.\n\nFehler: {ex.Message}",
"DLL nicht gefunden", MessageBoxButton.OK, MessageBoxImage.Warning);
services.AddSingleton<ICanonAPI>(new CanonAPIMock());
}
}
private void StartBackgroundServices()
{
try
{
var logger = _serviceProvider!.GetRequiredService<Logger>();
var uploadQueueService = _serviceProvider.GetRequiredService<PhotoPrismUploadQueueService>();
uploadQueueService.Start();
uploadQueueService.ScanAndQueueFailedUploads();
logger.Info("PhotoPrism UploadQueueService gestartet");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error starting background services: {ex.Message}");
}
}
} }

View File

@ -15,274 +15,115 @@ namespace CamBooth.App.Features.Camera;
public class CameraService : IDisposable public class CameraService : IDisposable
{ {
private readonly AppSettingsService _appSettings; private readonly AppSettingsService _appSettings;
private readonly Logger _logger; private readonly Logger _logger;
private readonly PictureGalleryService _pictureGalleryService; private readonly PictureGalleryService _pictureGalleryService;
private readonly PhotoPrismUploadQueueService _photoPrismUploadQueueService; private readonly PhotoPrismUploadQueueService _photoPrismUploadQueueService;
private readonly ICanonAPI _canonApi;
private readonly ICanonAPI _APIHandler; private ICamera? _mainCamera;
private List<ICamera>? _camList;
private bool _isConnected;
private CameraValue[] AvList; /// <summary>Fires whenever the camera delivers a new live-view frame.</summary>
public event Action<Stream>? LiveViewUpdated;
private int BulbTime = 30; public bool IsConnected => _isConnected && _mainCamera?.SessionOpen == true;
private List<ICamera> CamList;
private int ErrCount;
private object ErrLock = new();
private bool IsInit;
private CameraValue[] ISOList;
public ICamera? _mainCamera;
private CameraValue[] TvList;
public CameraService(Logger logger, public CameraService(
Logger logger,
AppSettingsService appSettings, AppSettingsService appSettings,
PictureGalleryService pictureGalleryService, PictureGalleryService pictureGalleryService,
PhotoPrismUploadQueueService photoPrismUploadQueueService, PhotoPrismUploadQueueService photoPrismUploadQueueService,
ICanonAPI APIHandler) ICanonAPI canonApi)
{ {
this._logger = logger; _logger = logger;
this._appSettings = appSettings; _appSettings = appSettings;
this._pictureGalleryService = pictureGalleryService; _pictureGalleryService = pictureGalleryService;
this._photoPrismUploadQueueService = photoPrismUploadQueueService; _photoPrismUploadQueueService = photoPrismUploadQueueService;
this._APIHandler = APIHandler; _canonApi = canonApi;
try
{
this.IsInit = true;
}
catch (DllNotFoundException)
{
this.ReportError("Canon DLLs not found!");
}
catch (Exception ex)
{
this.ReportError(ex.Message);
}
} }
/// <summary> Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. </summary>
public void Dispose() public void Dispose()
{ {
this.CloseSession(); CloseSession();
this.IsInit = false; _canonApi.Dispose();
this._APIHandler.Dispose(); _mainCamera?.Dispose();
this._mainCamera?.Dispose();
} }
public void ConnectCamera() public void ConnectCamera()
{ {
ErrorHandler.SevereErrorHappened += this.ErrorHandler_SevereErrorHappened; ErrorHandler.SevereErrorHappened += ErrorHandler_SevereErrorHappened;
ErrorHandler.NonSevereErrorHappened += this.ErrorHandler_NonSevereErrorHappened; ErrorHandler.NonSevereErrorHappened += ErrorHandler_NonSevereErrorHappened;
try try
{ {
this.RefreshCamera(); RefreshCameraList();
// Retry logic for camera detection (some systems need time to initialize) const int maxRetries = 3;
int maxRetries = 3; const int retryDelayMs = 750;
int retryDelay = 750; // milliseconds
for (int attempt = 0; attempt < maxRetries; attempt++) for (int attempt = 0; attempt < maxRetries; attempt++)
{ {
if (this.CamList != null && this.CamList.Any()) if (_camList?.Any() == true) break;
{
break;
}
if (attempt < maxRetries - 1) if (attempt < maxRetries - 1)
{ {
System.Threading.Thread.Sleep(retryDelay); System.Threading.Thread.Sleep(retryDelayMs);
this.RefreshCamera(); RefreshCameraList();
} }
} }
if (this.CamList == null || !this.CamList.Any()) if (_camList?.Any() != true)
{ throw new InvalidOperationException("No cameras found after multiple attempts.");
this.ReportError("No cameras / devices found");
throw new InvalidOperationException("No cameras / devices found after multiple attempts");
}
string cameraDeviceNames = string.Join(", ", this.CamList.Select(cam => cam.DeviceName)); _mainCamera = _camList[0];
this._logger.Debug(cameraDeviceNames); _logger.Info($"Camera found: {_mainCamera.DeviceName}");
// Update _mainCamera reference to the freshly detected camera OpenSession();
this._mainCamera = this.CamList[0]; SetSaveToComputer();
this._logger.Info($"Selected camera: {this._mainCamera.DeviceName}"); StartLiveView();
_isConnected = true;
this.OpenSession();
this.SetSettingSaveToComputer();
this.StarLiveView();
} }
catch (Exception ex) catch (Exception ex)
{ {
this._logger.Error($"Error connecting camera: {ex.Message}"); _logger.Error($"Error connecting camera: {ex.Message}");
throw; throw;
} }
} }
private void SetSettingSaveToComputer()
{
this._mainCamera.SetSetting(PropertyID.SaveTo, (int)SaveTo.Host);
this._mainCamera.SetCapacity(4096, int.MaxValue);
}
public void CloseSession() public void CloseSession()
{ {
try try
{ {
if (this._mainCamera != null && this._mainCamera.SessionOpen) if (_mainCamera?.SessionOpen == true)
{ {
this._mainCamera.CloseSession(); _mainCamera.CloseSession();
this._logger.Info("Camera session closed"); _logger.Info("Camera session closed");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
this._logger.Error($"Error closing camera session: {ex.Message}"); _logger.Error($"Error closing camera session: {ex.Message}");
} }
// AvCoBox.Items.Clear(); _isConnected = false;
// TvCoBox.Items.Clear();
// ISOCoBox.Items.Clear();
// SettingsGroupBox.IsEnabled = false;
// LiveViewGroupBox.IsEnabled = false;
// SessionButton.Content = "Open Session";
// SessionLabel.Content = "No open session";
// StarLVButton.Content = "Start LV";
}
private void RefreshCamera()
{
// CameraListBox.Items.Clear();
this.CamList = this._APIHandler.GetCameraList();
// foreach (Camera cam in CamList) CameraListBox.Items.Add(cam.DeviceName);
// if (_mainCamera?.SessionOpen == true) CameraListBox.SelectedIndex = CamList.FindIndex(t => t.ID == _mainCamera.ID);
// else if (CamList.Count > 0) CameraListBox.SelectedIndex = 0;
}
private void OpenSession()
{
if (this._mainCamera == null)
{
throw new InvalidOperationException("Camera reference is null. Make sure ConnectCamera is called first.");
}
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}");
const int maxRetries = 3;
const int retryDelayMs = 1000;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
this._mainCamera.OpenSession();
break;
}
catch (Exception ex) when (attempt < maxRetries - 1 && IsSessionNotOpenError(ex))
{
this._logger.Warning($"OpenSession attempt {attempt + 1}/{maxRetries} failed ({ex.Message}), refreshing camera and retrying in {retryDelayMs}ms...");
System.Threading.Thread.Sleep(retryDelayMs);
this.RefreshCamera();
if (this.CamList?.Any() == true)
{
this._mainCamera = this.CamList[0];
}
}
catch (Exception ex)
{
this._logger.Error($"Failed to open camera session: {ex.Message}");
this.ReportError($"Failed to open camera session: {ex.Message}");
throw;
}
}
this._logger.Info("Camera session opened successfully");
//_mainCamera.ProgressChanged += MainCamera_ProgressChanged;
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);
// ISOCoBox.SelectedIndex = ISOCoBox.Items.IndexOf(ISOValues.GetValue(_mainCamera.GetInt32Setting(PropertyID.ISO)).StringValue);
// SettingsGroupBox.IsEnabled = true;
// LiveViewGroupBox.IsEnabled = true;
}
private void ReportError(string message)
{
this._logger.Info(message);
}
private static bool IsSessionNotOpenError(Exception ex)
{
const string errorName = "SESSION_NOT_OPEN";
return ex.Message.Contains(errorName) || (ex.InnerException?.Message?.Contains(errorName) ?? false);
}
private void StarLiveView()
{
try
{
if (!this._mainCamera.IsLiveViewOn)
{
this._mainCamera.StartLiveView();
}
else
{
this._mainCamera.StopLiveView();
//LVCanvas.Background = Brushes.LightGray;
}
}
catch (Exception ex)
{
this.ReportError(ex.Message);
}
} }
public void TakePhoto() public void TakePhoto()
{ {
try if (_mainCamera == null) throw new InvalidOperationException("Camera not connected.");
{ _mainCamera.TakePhoto();
this._mainCamera.TakePhoto();
}
catch (Exception ex)
{
this.ReportError(ex.Message);
throw;
}
} }
public async Task PrepareFocusAsync(int focusTimeoutMs = 1500) public async Task PrepareFocusAsync(int focusTimeoutMs = 1500)
{ {
if (this._mainCamera is not EOSDigital.API.Camera sdkCamera) if (_mainCamera is not EOSDigital.API.Camera sdkCamera)
{ {
await Task.Delay(200); await Task.Delay(200);
return; return;
@ -293,25 +134,21 @@ public class CameraService : IDisposable
void FocusStateChanged(EOSDigital.API.Camera sender, StateEventID eventId, int parameter) void FocusStateChanged(EOSDigital.API.Camera sender, StateEventID eventId, int parameter)
{ {
if (eventId == StateEventID.AfResult) if (eventId == StateEventID.AfResult)
{
focusCompleted.TrySetResult(true); focusCompleted.TrySetResult(true);
} }
}
sdkCamera.StateChanged += FocusStateChanged; sdkCamera.StateChanged += FocusStateChanged;
try try
{ {
await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.Halfway)); await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.Halfway));
var completedTask = await Task.WhenAny(focusCompleted.Task, Task.Delay(focusTimeoutMs)); var completed = await Task.WhenAny(focusCompleted.Task, Task.Delay(focusTimeoutMs));
if (completedTask != focusCompleted.Task) if (completed != focusCompleted.Task)
{ _logger.Info("Autofocus timeout reached, continuing.");
this._logger.Info("Autofocus timeout reached, continuing with countdown.");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
this.ReportError(ex.Message); _logger.Error(ex.Message);
} }
finally finally
{ {
@ -319,120 +156,142 @@ public class CameraService : IDisposable
{ {
await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.OFF)); await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.OFF));
} }
catch (Exception ex) catch (Exception ex) { _logger.Error(ex.Message); }
{
this.ReportError(ex.Message);
}
sdkCamera.StateChanged -= FocusStateChanged; sdkCamera.StateChanged -= FocusStateChanged;
} }
} }
#region API Events private void RefreshCameraList() => _camList = _canonApi.GetCameraList();
// private void APIHandler_CameraAdded(CanonAPI sender)
// { private void OpenSession()
// try {
// { if (_mainCamera == null)
// } throw new InvalidOperationException("Camera reference is null.");
// catch (Exception ex)
// { if (_mainCamera.SessionOpen)
// ReportError(ex.Message, false); {
// } _logger.Info($"Session already open for {_mainCamera.DeviceName}");
// } return;
}
_logger.Info($"Opening session for: {_mainCamera.DeviceName}");
const int maxRetries = 3;
const int retryDelayMs = 1000;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
_mainCamera.OpenSession();
break;
}
catch (Exception ex) when (attempt < maxRetries - 1 && IsSessionNotOpenError(ex))
{
_logger.Warning($"OpenSession attempt {attempt + 1}/{maxRetries} failed, retrying...");
System.Threading.Thread.Sleep(retryDelayMs);
RefreshCameraList();
if (_camList?.Any() == true)
_mainCamera = _camList[0];
}
catch (Exception ex)
{
_logger.Error($"Failed to open session: {ex.Message}");
throw;
}
}
_logger.Info("Session opened successfully");
_mainCamera.StateChanged += MainCamera_StateChanged;
_mainCamera.DownloadReady += MainCamera_DownloadReady;
_mainCamera.LiveViewUpdated += MainCamera_LiveViewUpdated;
}
private void SetSaveToComputer()
{
_mainCamera!.SetSetting(PropertyID.SaveTo, (int)SaveTo.Host);
_mainCamera.SetCapacity(4096, int.MaxValue);
}
private void StartLiveView()
{
try
{
if (!_mainCamera!.IsLiveViewOn)
_mainCamera.StartLiveView();
else
_mainCamera.StopLiveView();
}
catch (Exception ex)
{
_logger.Error(ex.Message);
}
}
private static bool IsSessionNotOpenError(Exception ex)
{
const string errorName = "SESSION_NOT_OPEN";
return ex.Message.Contains(errorName) || (ex.InnerException?.Message?.Contains(errorName) ?? false);
}
#region Camera event handlers
private void MainCamera_LiveViewUpdated(ICamera sender, Stream img) =>
LiveViewUpdated?.Invoke(img);
private void MainCamera_StateChanged(EOSDigital.API.Camera sender, StateEventID eventID, int parameter) private void MainCamera_StateChanged(EOSDigital.API.Camera sender, StateEventID eventID, int parameter)
{ {
try try
{ {
if (eventID == StateEventID.Shutdown && this.IsInit) if (eventID == StateEventID.Shutdown && _isConnected)
{ Application.Current.Dispatcher.Invoke(CloseSession);
Application.Current.Dispatcher.Invoke(() => this.CloseSession());
//Dispatcher.Invoke((Action)delegate { CloseSession(); });
}
} }
catch (Exception ex) catch (Exception ex)
{ {
this.ReportError(ex.Message); _logger.Error(ex.Message);
} }
} }
// private void MainCamera_ProgressChanged(object sender, int progress) private void MainCamera_DownloadReady(ICamera sender, IDownloadInfo info)
// {
// try
// {
// //MainProgressBar.Dispatcher.Invoke((Action)delegate { MainProgressBar.Value = progress; });
// }
// catch (Exception ex)
// {
// ReportError(ex.Message, false);
// }
// }
// private void MainCamera_LiveViewUpdated(EOSDigital.API.Camera sender, Stream img)
// {
// try
// {
// using (WrapStream s = new WrapStream(img))
// {
// img.Position = 0;
// BitmapImage EvfImage = new BitmapImage();
// EvfImage.BeginInit();
// EvfImage.StreamSource = s;
// EvfImage.CacheOption = BitmapCacheOption.OnLoad;
// EvfImage.EndInit();
// EvfImage.Freeze();
// Application.Current.Dispatcher.BeginInvoke(SetImageAction, EvfImage);
// }
// }
// catch (Exception ex)
// {
// ReportError(ex.Message, false);
// }
// }
private void MainCamera_DownloadReady(ICamera sender, IDownloadInfo Info)
{ {
this._logger.Info("MainCamera_DownloadReady called"); _logger.Info("Download ready");
try try
{ {
Info.FileName = $"img_{Guid.NewGuid().ToString()}.jpg"; info.FileName = $"img_{Guid.NewGuid()}.jpg";
sender.DownloadFile(Info, this._appSettings.PictureLocation); sender.DownloadFile(info, _appSettings.PictureLocation);
var savedPhotoPath = Path.Combine(this._appSettings.PictureLocation, Info.FileName); var savedPath = Path.Combine(_appSettings.PictureLocation!, info.FileName);
this._logger.Info("Download complete: " + savedPhotoPath); _logger.Info($"Download complete: {savedPath}");
Application.Current.Dispatcher.Invoke(() => { Application.Current.Dispatcher.Invoke(() =>
this._pictureGalleryService.IncrementNewPhotoCount(); {
this._pictureGalleryService.LoadThumbnailsToCache(); _pictureGalleryService.IncrementNewPhotoCount();
_pictureGalleryService.LoadThumbnailsToCache();
}); });
// Füge neues Foto zur PhotoPrism Upload-Queue hinzu (wenn Auto-Upload aktiviert) _photoPrismUploadQueueService.QueueNewPhoto(savedPath);
this._photoPrismUploadQueueService.QueueNewPhoto(savedPhotoPath);
this._logger.Info($"Foto zur PhotoPrism Upload-Queue hinzugefügt: {Info.FileName}");
} }
catch (Exception ex) catch (Exception ex)
{ {
this.ReportError(ex.Message); _logger.Error(ex.Message);
} }
} }
private void ErrorHandler_NonSevereErrorHappened(object sender, ErrorCode ex) private void ErrorHandler_NonSevereErrorHappened(object sender, ErrorCode ex) =>
{ _logger.Error($"SDK Error: {ex} (0x{(int)ex:X})");
this.ReportError($"SDK Error code: {ex} ({((int)ex).ToString("X")})");
}
private void ErrorHandler_SevereErrorHappened(object sender, Exception ex) private void ErrorHandler_SevereErrorHappened(object sender, Exception ex) =>
{ _logger.Error(ex.Message);
this.ReportError(ex.Message);
}
#endregion #endregion
} }

View File

@ -1,10 +1,9 @@
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using CamBooth.App.Core.AppSettings;
using CamBooth.App.Core.Logging; using CamBooth.App.Core.Logging;
using CamBooth.App.Features.Camera; using CamBooth.App.Features.Camera;
@ -12,87 +11,69 @@ using EOSDigital.API;
namespace CamBooth.App.Features.LiveView; namespace CamBooth.App.Features.LiveView;
public partial class LiveViewPage : Page public partial class LiveViewPage : Page, IDisposable
{ {
private readonly AppSettingsService _appSettings;
private readonly CameraService _cameraService; private readonly CameraService _cameraService;
private readonly Logger _logger; private readonly Logger _logger;
private readonly ImageBrush _bgBrush = new();
private readonly ImageBrush bgbrush = new();
private readonly Action<BitmapImage> SetImageAction;
public LiveViewPage(Logger logger, AppSettingsService appSettings, CameraService cameraService) public LiveViewPage(Logger logger, CameraService cameraService)
{ {
this._logger = logger; _logger = logger;
this._appSettings = appSettings; _cameraService = cameraService;
this._cameraService = cameraService;
this.InitializeComponent();
this.SetImageAction = img => { this.bgbrush.ImageSource = img; };
// Configure the image brush InitializeComponent();
this.bgbrush.Stretch = Stretch.UniformToFill;
this.bgbrush.AlignmentX = AlignmentX.Center;
this.bgbrush.AlignmentY = AlignmentY.Center;
this.LVCanvas.Background = this.bgbrush; _bgBrush.Stretch = Stretch.UniformToFill;
_bgBrush.AlignmentX = AlignmentX.Center;
_bgBrush.AlignmentY = AlignmentY.Center;
LVCanvas.Background = _bgBrush;
// Apply horizontal flip on the Canvas using RenderTransform var transformGroup = new TransformGroup();
TransformGroup transformGroup = new();
transformGroup.Children.Add(new ScaleTransform { ScaleX = -1, ScaleY = 1 }); transformGroup.Children.Add(new ScaleTransform { ScaleX = -1, ScaleY = 1 });
transformGroup.Children.Add(new TranslateTransform { X = 1, Y = 0 }); transformGroup.Children.Add(new TranslateTransform { X = 1, Y = 0 });
this.LVCanvas.RenderTransform = transformGroup; LVCanvas.RenderTransform = transformGroup;
this.LVCanvas.RenderTransformOrigin = new Point(0.5, 0.5); LVCanvas.RenderTransformOrigin = new Point(0.5, 0.5);
try try
{ {
cameraService.ConnectCamera(); cameraService.ConnectCamera();
cameraService.LiveViewUpdated += OnLiveViewUpdated;
// Verify that camera session is open before subscribing to events _logger.Info("LiveViewPage initialized successfully");
if (cameraService._mainCamera != null && cameraService._mainCamera.SessionOpen)
{
cameraService._mainCamera.LiveViewUpdated += this.MainCamera_OnLiveViewUpdated;
this._logger.Info("LiveViewPage initialized successfully");
}
else
{
this._logger.Error("Camera session is not open after connection attempt");
throw new InvalidOperationException("Camera session failed to open");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
this._logger.Error($"Failed to initialize LiveViewPage: {ex.Message}"); _logger.Error($"Failed to initialize LiveViewPage: {ex.Message}");
throw; throw;
} }
} }
private void MainCamera_OnLiveViewUpdated(ICamera sender, Stream img) public void Dispose()
{
_cameraService.LiveViewUpdated -= OnLiveViewUpdated;
_cameraService.Dispose();
}
private void OnLiveViewUpdated(Stream img)
{ {
try try
{ {
using WrapStream s = new(img); using WrapStream s = new(img);
img.Position = 0; img.Position = 0;
BitmapImage EvfImage = new(); var evfImage = new BitmapImage();
EvfImage.BeginInit(); evfImage.BeginInit();
EvfImage.StreamSource = s; evfImage.StreamSource = s;
EvfImage.CacheOption = BitmapCacheOption.OnLoad; evfImage.CacheOption = BitmapCacheOption.OnLoad;
EvfImage.EndInit(); evfImage.EndInit();
EvfImage.Freeze(); evfImage.Freeze();
Application.Current.Dispatcher.BeginInvoke(this.SetImageAction, EvfImage); Application.Current.Dispatcher.BeginInvoke(() => _bgBrush.ImageSource = evfImage);
} }
catch (Exception ex) catch (Exception ex)
{ {
this._logger.Error(ex.Message); _logger.Error(ex.Message);
} }
} }
public void Dispose()
{
this._cameraService.Dispose();
}
} }

View File

@ -1,14 +1,11 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
using System.Windows.Threading; using System.Windows.Threading;
using CamBooth.App.Core.AppSettings; using CamBooth.App.Core.AppSettings;
using CamBooth.App.Core.Logging; using CamBooth.App.Core.Logging;
using CamBooth.App.Features.Camera; using CamBooth.App.Features.Camera;
@ -16,7 +13,6 @@ using CamBooth.App.Features.DebugConsole;
using CamBooth.App.Features.LiveView; using CamBooth.App.Features.LiveView;
using CamBooth.App.Features.PhotoPrismUpload; using CamBooth.App.Features.PhotoPrismUpload;
using CamBooth.App.Features.PictureGallery; using CamBooth.App.Features.PictureGallery;
using Wpf.Ui.Controls; using Wpf.Ui.Controls;
namespace CamBooth.App; namespace CamBooth.App;
@ -27,37 +23,23 @@ namespace CamBooth.App;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private readonly Logger _logger; private readonly Logger _logger;
private readonly AppSettingsService _appSettings; private readonly AppSettingsService _appSettings;
private readonly PictureGalleryService _pictureGalleryService; private readonly PictureGalleryService _pictureGalleryService;
private readonly CameraService _cameraService; private readonly CameraService _cameraService;
private readonly PhotoPrismUploadService _photoPrismUploadService; private readonly PhotoPrismUploadService _photoPrismUploadService;
private readonly MainWindowViewModel _viewModel;
private bool _isDebugConsoleVisible = true;
private bool _isPicturePanelVisible = false;
private LiveViewPage? _liveViewPage; private LiveViewPage? _liveViewPage;
private bool _isPhotoProcessRunning;
private bool _isCameraStarted; private bool _isCameraStarted;
private bool _isPicturePanelVisible;
private bool _isDebugConsoleVisible;
private bool _isShutdownSliderOpen; private bool _isShutdownSliderOpen;
private const string ShutdownGlyphClosed = "\uE7E8"; private const string ShutdownGlyphClosed = "\uE7E8";
private const string ShutdownGlyphOpen = "\uE711"; private const string ShutdownGlyphOpen = "\uE711";
private const double ShutdownSliderOffset = 160; private const double ShutdownSliderOffset = 160;
private readonly DispatcherTimer _focusStatusAnimationTimer = new() { Interval = TimeSpan.FromMilliseconds(250) }; private readonly DispatcherTimer _focusStatusAnimationTimer = new() { Interval = TimeSpan.FromMilliseconds(250) };
private readonly DispatcherTimer _galleryPromptTimer = new() { Interval = TimeSpan.FromSeconds(5) };
private int _focusStatusDots; private int _focusStatusDots;
@ -66,545 +48,207 @@ public partial class MainWindow : Window
AppSettingsService appSettings, AppSettingsService appSettings,
PictureGalleryService pictureGalleryService, PictureGalleryService pictureGalleryService,
CameraService cameraService, CameraService cameraService,
PhotoPrismUploadService photoPrismUploadService) PhotoPrismUploadService photoPrismUploadService,
MainWindowViewModel viewModel)
{ {
this._logger = logger; _logger = logger;
this._appSettings = appSettings; _appSettings = appSettings;
this._pictureGalleryService = pictureGalleryService; _pictureGalleryService = pictureGalleryService;
this._cameraService = cameraService; _cameraService = cameraService;
this._photoPrismUploadService = photoPrismUploadService; _photoPrismUploadService = photoPrismUploadService;
_viewModel = viewModel;
InitializeComponent(); InitializeComponent();
this.SetVisibilityDebugConsole(_appSettings.IsDebugConsoleVisible);
this.SetVisibilityPicturePanel(this._isPicturePanelVisible);
// Lade Thumbnails asynchron und zeige dann den Welcome Screen // Wire ViewModel events to UI
_ = InitializeAsync(); _viewModel.LoadingProgressChanged += (status, count) =>
this.Closing += OnClosing;
TimerControlRectangleAnimation.OnTimerEllapsed += TimerControlRectangleAnimation_OnTimerEllapsed;
this._focusStatusAnimationTimer.Tick += (_, _) =>
{ {
this._focusStatusDots = (this._focusStatusDots + 1) % 4; LoadingStatusText.Text = status;
this.CaptureStatusText.Text = $"Scharfstellen{new string('.', this._focusStatusDots)}"; LoadingCountText.Text = count;
}; };
this._galleryPromptTimer.Tick += (_, _) => this.HideGalleryPrompt(); _viewModel.InitializationCompleted += () =>
{
LoadingOverlay.Visibility = Visibility.Collapsed;
WelcomeOverlay.Visibility = Visibility.Visible;
};
_viewModel.GalleryPromptRequested += () => GalleryPrompt.Visibility = Visibility.Visible;
_viewModel.GalleryPromptDismissed += () => GalleryPrompt.Visibility = Visibility.Collapsed;
// Subscribe to new photo count changes // Wire service events
this._pictureGalleryService.NewPhotoCountChanged += PictureGalleryService_NewPhotoCountChanged; _pictureGalleryService.NewPhotoCountChanged += OnNewPhotoCountChanged;
TimerControlRectangleAnimation.OnTimerEllapsed += OnTimerElapsed;
this.DebugCloseButton.Visibility = Visibility.Collapsed; // Focus animation timer
this.HideDebugButton.Visibility = this._appSettings.IsDebugConsoleVisible ? Visibility.Visible : Visibility.Collapsed; _focusStatusAnimationTimer.Tick += (_, _) =>
{
_focusStatusDots = (_focusStatusDots + 1) % 4;
CaptureStatusText.Text = $"Scharfstellen{new string('.', _focusStatusDots)}";
};
// Initialize PhotoPrism upload if auto-upload is enabled // Initial UI state
if (appSettings.PhotoPrismAutoUploadEnabled) _isDebugConsoleVisible = _appSettings.IsDebugConsoleVisible;
{ SetVisibilityDebugConsole(_isDebugConsoleVisible);
logger.Info("PhotoPrism Auto-Upload ist aktiviert. Authentifiziere..."); SetVisibilityPicturePanel(false);
_ = Task.Run(async () => DebugCloseButton.Visibility = Visibility.Collapsed;
{ HideDebugButton.Visibility = _appSettings.IsDebugConsoleVisible ? Visibility.Visible : Visibility.Collapsed;
var authSuccess = await _photoPrismUploadService.AuthenticateAsync();
if (authSuccess) Closing += OnClosing;
{
logger.Info("PhotoPrism-Authentifizierung erfolgreich!"); _ = _viewModel.InitializeAsync();
} _logger.Info($"config file loaded: '{appSettings.ConfigFileName}'");
else _logger.Info("MainWindow initialized");
{
logger.Warning("PhotoPrism-Authentifizierung fehlgeschlagen. Auto-Upload wird nicht funktionieren.");
}
});
} }
logger.Info($"config file loaded: '{appSettings.ConfigFileName}'");
logger.Info("MainWindow initialized");
}
private async Task InitializeAsync() #region Event handlers (wired in XAML or in ctor)
private void OnTimerElapsed()
{ {
try try
{ {
// Zeige Ladeanzeige _viewModel.OnTimerElapsed();
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) catch (Exception ex)
{ {
this._logger.Error($"Initialization error: {ex.Message}"); _logger.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()
{
var photoTakenSuccessfully = false;
try
{
this._cameraService.TakePhoto();
photoTakenSuccessfully = true;
}
catch (Exception exception)
{
//TODO: mit content dialog ersetzen
System.Windows.MessageBox.Show("Sorry, da ging was schief! Bitte nochmal probieren."); System.Windows.MessageBox.Show("Sorry, da ging was schief! Bitte nochmal probieren.");
this._logger.Info(exception.Message);
} }
finally finally
{ {
this.StopFocusStatusAnimation(); StopFocusStatusAnimation();
this.CaptureStatusText.Visibility = Visibility.Collapsed; CaptureStatusText.Visibility = Visibility.Collapsed;
SwitchButtonAndTimerPanel(); SwitchButtonAndTimerPanel();
this._isPhotoProcessRunning = false; }
if (photoTakenSuccessfully) }
private void OnNewPhotoCountChanged(object? sender, int count)
{ {
this.ShowGalleryPrompt(); NewPhotosBadge.Visibility = count > 0 ? Visibility.Visible : Visibility.Collapsed;
if (count > 0)
NewPhotoCountText.Text = count.ToString();
} }
}
}
private void SetVisibilityPicturePanel(bool visibility)
{
if (visibility)
{
this.HideGalleryPrompt();
this.PicturePanel.Navigate(new PictureGalleryPage(this._appSettings, this._logger, this._pictureGalleryService, this._photoPrismUploadService));
// 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;
}
public void ClosePicturePanel()
{
if (this.PicturePanel.Content is PictureGalleryPage pictureGalleryPage)
{
pictureGalleryPage.CloseOpenDialog();
}
if (this.PicturePanel.Content is null)
{
return;
}
this.SetVisibilityPicturePanel(false);
}
private void OnClosing(object? sender, CancelEventArgs e) private void OnClosing(object? sender, CancelEventArgs e)
{ {
this.CloseCameraSessionSafely();
this._liveViewPage?.Dispose();
}
private void StartExperience(object sender, RoutedEventArgs e)
{
this.StartLiveViewIfNeeded();
this.WelcomeOverlay.Visibility = Visibility.Collapsed;
this.ButtonPanel.Visibility = Visibility.Visible;
this.ActionButtonsContainer.Visibility = Visibility.Visible;
this.ShutdownDock.Visibility = Visibility.Visible;
}
private void StartLiveViewIfNeeded()
{
if (this._isCameraStarted)
{
return;
}
try try
{ {
this._liveViewPage = new LiveViewPage(this._logger, this._appSettings, this._cameraService); _cameraService.CloseSession();
this.MainFrame.Navigate(this._liveViewPage);
this._isCameraStarted = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
this._logger.Error($"Failed to start live view: {ex.Message}\n{ex.StackTrace}"); _logger.Error($"Fehler beim Schließen der Kamera-Session: {ex.Message}");
System.Windows.MessageBox.Show($"Failed to initialize camera. Please ensure the camera is properly connected.\n\nError: {ex.Message}",
"Camera Connection Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
}
} }
_liveViewPage?.Dispose();
}
private void SetVisibilityDebugConsole(object sender, RoutedEventArgs e) // XAML-bound handlers
private void StartExperience(object sender, RoutedEventArgs e)
{ {
this.SetVisibilityDebugConsole(this._isDebugConsoleVisible); StartLiveViewIfNeeded();
WelcomeOverlay.Visibility = Visibility.Collapsed;
ButtonPanel.Visibility = Visibility.Visible;
ActionButtonsContainer.Visibility = Visibility.Visible;
ShutdownDock.Visibility = Visibility.Visible;
} }
private void SetVisibilityDebugConsole(bool visibility)
{
if (!_appSettings.IsDebugConsoleVisible)
{
return;
}
if (visibility)
{
this.DebugFrame.Navigate(new DebugConsolePage(this._logger));
}
else
{
this.DebugFrame.ClearValue(MainWindow.ContentProperty);
}
this._isDebugConsoleVisible = !visibility;
}
private async void StartTakePhotoProcess(object sender, RoutedEventArgs e) private async void StartTakePhotoProcess(object sender, RoutedEventArgs e)
{ {
this.HideGalleryPrompt(); if (_viewModel.IsPhotoProcessRunning) return;
this.ClosePicturePanel();
if (this._isPhotoProcessRunning) ClosePicturePanel();
{ _viewModel.StartPhotoProcess();
return;
}
this._isPhotoProcessRunning = true;
try try
{ {
SwitchButtonAndTimerPanel(); SwitchButtonAndTimerPanel();
TimerControlRectangleAnimation.StartTimer(_appSettings.PhotoCountdownSeconds);
StartFocusStatusAnimation();
CaptureStatusText.Visibility = Visibility.Visible;
TimerControlRectangleAnimation.StartTimer(this._appSettings.PhotoCountdownSeconds); await Task.Delay(TimeSpan.FromSeconds(_appSettings.FocusDelaySeconds));
this.StartFocusStatusAnimation(); if (!_viewModel.IsPhotoProcessRunning) return;
this.CaptureStatusText.Visibility = Visibility.Visible;
await Task.Delay(TimeSpan.FromSeconds(this._appSettings.FocusDelaySeconds));
if (!this._isPhotoProcessRunning)
{
return;
}
await this._cameraService.PrepareFocusAsync(focusTimeoutMs: this._appSettings.FocusTimeoutMs); await _cameraService.PrepareFocusAsync(_appSettings.FocusTimeoutMs);
this.StopFocusStatusAnimation(); StopFocusStatusAnimation();
this.CaptureStatusText.Visibility = Visibility.Collapsed; CaptureStatusText.Visibility = Visibility.Collapsed;
}
catch (Exception exception)
{
this._isPhotoProcessRunning = false;
this.StopFocusStatusAnimation();
this.CaptureStatusText.Visibility = Visibility.Collapsed;
if (this.TimerPanel.Visibility == Visibility.Visible)
{
SwitchButtonAndTimerPanel();
}
this._logger.Error(exception.Message);
}
}
private void SwitchButtonAndTimerPanel()
{
this.ButtonPanel.Visibility = this.ButtonPanel.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, MouseButtonEventArgs e)
{
this.SetVisibilityPicturePanel(this._isPicturePanelVisible);
}
private void CloseApp(object sender, RoutedEventArgs e)
{
this.Close();
}
private async void ShutdownWindows(object sender, RoutedEventArgs e)
{
// Show confirmation dialog
var confirmDialog = new ContentDialog(this.DialogPresenter);
confirmDialog.Title = "Sicherheitsabfrage";
confirmDialog.Content = "Möchtest du die Fotobox wirklich ausschalten?";
confirmDialog.PrimaryButtonText = "Ja, ausschalten";
confirmDialog.CloseButtonText = "Abbrechen";
confirmDialog.DefaultButton = ContentDialogButton.Close;
confirmDialog.PrimaryButtonAppearance = ControlAppearance.Danger;
confirmDialog.CloseButtonAppearance = ControlAppearance.Secondary;
confirmDialog.Background = new SolidColorBrush(Colors.White);
confirmDialog.Foreground = new SolidColorBrush(Colors.Black);
var result = await confirmDialog.ShowAsync();
// Only proceed with shutdown if user confirmed
if (result != ContentDialogResult.Primary)
{
return;
}
// Kamera-Session vor Abmeldung/Shutdown sauber schließen.
this.CloseCameraSessionSafely();
if (this._appSettings.IsShutdownEnabled)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = "shutdown",
Arguments = "/s /t 0",
CreateNoWindow = true,
UseShellExecute = false
});
}
catch (Exception exception)
{
this._logger.Error(exception.Message);
System.Windows.MessageBox.Show("Windows konnte nicht heruntergefahren werden. Bitte erneut versuchen.");
}
}
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 CloseCameraSessionSafely()
{
try
{
this._cameraService.CloseSession();
} }
catch (Exception ex) catch (Exception ex)
{ {
this._logger.Error($"Fehler beim Schließen der Kamera-Session: {ex.Message}"); _viewModel.CancelPhotoProcess();
StopFocusStatusAnimation();
CaptureStatusText.Visibility = Visibility.Collapsed;
if (TimerPanel.Visibility == Visibility.Visible)
SwitchButtonAndTimerPanel();
_logger.Error(ex.Message);
} }
} }
private void SetVisibilityPicturePanel(object sender, MouseButtonEventArgs e) =>
SetVisibilityPicturePanel(!_isPicturePanelVisible);
private void SetVisibilityDebugConsole(object sender, RoutedEventArgs e)
{
_isDebugConsoleVisible = !_isDebugConsoleVisible;
SetVisibilityDebugConsole(_isDebugConsoleVisible);
}
private void ToggleShutdownSlider(object sender, RoutedEventArgs e) private void ToggleShutdownSlider(object sender, RoutedEventArgs e)
{ {
this._isShutdownSliderOpen = !this._isShutdownSliderOpen; _isShutdownSliderOpen = !_isShutdownSliderOpen;
this.ShutdownToggleButton.Content = this._isShutdownSliderOpen ? ShutdownGlyphOpen : ShutdownGlyphClosed; ShutdownToggleButton.Content = _isShutdownSliderOpen ? ShutdownGlyphOpen : ShutdownGlyphClosed;
this.AnimateShutdownSlider(this._isShutdownSliderOpen);
}
private void AnimateShutdownSlider(bool open)
{
var animation = new DoubleAnimation var animation = new DoubleAnimation
{ {
To = open ? 0 : ShutdownSliderOffset, To = _isShutdownSliderOpen ? 0 : ShutdownSliderOffset,
Duration = TimeSpan.FromMilliseconds(250), Duration = TimeSpan.FromMilliseconds(250),
EasingFunction = new QuadraticEase() EasingFunction = new QuadraticEase()
}; };
ShutdownSliderTransform.BeginAnimation(System.Windows.Media.TranslateTransform.XProperty, animation);
this.ShutdownSliderTransform.BeginAnimation(System.Windows.Media.TranslateTransform.XProperty, animation);
} }
private void StartFocusStatusAnimation() private async void ShutdownWindows(object sender, RoutedEventArgs e)
{ {
this._focusStatusDots = 0; var confirmDialog = new ContentDialog(DialogPresenter)
this.CaptureStatusText.Text = "Scharfstellen";
this._focusStatusAnimationTimer.Start();
}
private void StopFocusStatusAnimation()
{ {
this._focusStatusAnimationTimer.Stop(); Title = "Sicherheitsabfrage",
this._focusStatusDots = 0; Content = "Möchtest du die Fotobox wirklich ausschalten?",
} PrimaryButtonText = "Ja, ausschalten",
CloseButtonText = "Abbrechen",
DefaultButton = ContentDialogButton.Close,
PrimaryButtonAppearance = ControlAppearance.Danger,
CloseButtonAppearance = ControlAppearance.Secondary,
Background = new SolidColorBrush(Colors.White),
Foreground = new SolidColorBrush(Colors.Black)
};
private void ShowGalleryPrompt() if (await confirmDialog.ShowAsync() != ContentDialogResult.Primary) return;
{
this.GalleryPrompt.Visibility = Visibility.Visible;
this._galleryPromptTimer.Stop();
this._galleryPromptTimer.Start();
}
private void HideGalleryPrompt()
{
this._galleryPromptTimer.Stop();
this.GalleryPrompt.Visibility = Visibility.Collapsed;
}
private async void OpenGalleryFromPrompt(object sender, RoutedEventArgs e)
{
this.HideGalleryPrompt();
this.SetVisibilityPicturePanel(true);
// Wait a bit for the page to load before showing the dialog
await Task.Delay(300);
// Show the latest photo in a dialog
await this.ShowLatestPhotoDialogAsync();
}
private async Task ShowLatestPhotoDialogAsync()
{
try try
{ {
// Get the latest photo path _cameraService.CloseSession();
await this._pictureGalleryService.LoadThumbnailsToCache(1); }
var latestPhotos = this._pictureGalleryService.ThumbnailsOrderedByNewestDescending; catch
if (latestPhotos.Count == 0)
{ {
this._logger.Error("No photos found in gallery");
return;
} }
// Get the file path from the BitmapImage source var (args, errorMsg) = _appSettings.IsShutdownEnabled
var latestPhoto = latestPhotos[0]; ? ("/s /t 0", "Windows konnte nicht heruntergefahren werden.")
if (latestPhoto.UriSource == null) : ("/l", "Abmeldung fehlgeschlagen.");
{
this._logger.Error("Latest photo UriSource is null");
return;
}
string photoPath = Uri.UnescapeDataString(latestPhoto.UriSource.AbsolutePath); try
this._logger.Info($"Opening photo dialog for: {photoPath}");
// Get the current gallery page
if (this.PicturePanel.Content is PictureGalleryPage galleryPage)
{ {
// Show the photo dialog Process.Start(new ProcessStartInfo
await galleryPage.ShowPhotoDialogAsync(photoPath);
}
else
{ {
this._logger.Error("PicturePanel content is not a PictureGalleryPage"); FileName = "shutdown",
} Arguments = args,
CreateNoWindow = true,
UseShellExecute = false
});
} }
catch (Exception ex) catch (Exception ex)
{ {
this._logger.Error($"Error showing photo dialog: {ex.Message}"); _logger.Error(ex.Message);
} System.Windows.MessageBox.Show(errorMsg);
}
private void PictureGalleryService_NewPhotoCountChanged(object? sender, int newPhotoCount)
{
if (newPhotoCount > 0)
{
this.NewPhotosBadge.Visibility = Visibility.Visible;
this.NewPhotoCountText.Text = newPhotoCount.ToString();
}
else
{
this.NewPhotosBadge.Visibility = Visibility.Collapsed;
} }
} }
@ -612,28 +256,151 @@ public partial class MainWindow : Window
{ {
try try
{ {
this._logger.Info("Zeige PhotoPrism Album-QR-Code an..."); var qrCodeImage = _photoPrismUploadService.GenerateAlbumQRCode();
// Generiere QR-Code
var qrCodeImage = this._photoPrismUploadService.GenerateAlbumQRCode();
if (qrCodeImage == null) if (qrCodeImage == null)
{ {
System.Windows.MessageBox.Show("QR-Code konnte nicht generiert werden. Bitte überprüfe die PhotoPrism-Konfiguration.", System.Windows.MessageBox.Show(
"QR-Code konnte nicht generiert werden. Bitte überprüfe die PhotoPrism-Konfiguration.",
"Fehler", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); "Fehler", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
return; return;
} }
// Öffne QR-Code-Fenster
var qrWindow = new PhotoPrismQRCodeDisplayWindow(); var qrWindow = new PhotoPrismQRCodeDisplayWindow();
qrWindow.SetQRCode(qrCodeImage); qrWindow.SetQRCode(qrCodeImage);
qrWindow.ShowDialog(); qrWindow.ShowDialog();
} }
catch (Exception ex) catch (Exception ex)
{ {
this._logger.Error($"Fehler beim Anzeigen des PhotoPrism QR-Codes: {ex.Message}"); _logger.Error($"Fehler beim Anzeigen des QR-Codes: {ex.Message}");
System.Windows.MessageBox.Show($"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); "Fehler", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
} }
} }
private async void OpenGalleryFromPrompt(object sender, RoutedEventArgs e)
{
_viewModel.DismissGalleryPrompt();
SetVisibilityPicturePanel(true);
await Task.Delay(300);
await ShowLatestPhotoDialogAsync();
}
private void CloseApp(object sender, RoutedEventArgs e) => Close();
#endregion
#region UI helpers
private void StartLiveViewIfNeeded()
{
if (_isCameraStarted) return;
try
{
_liveViewPage = new LiveViewPage(_logger, _cameraService);
MainFrame.Navigate(_liveViewPage);
_isCameraStarted = true;
}
catch (Exception ex)
{
_logger.Error($"Failed to start live view: {ex.Message}\n{ex.StackTrace}");
System.Windows.MessageBox.Show(
$"Failed to initialize camera. Please ensure the camera is properly connected.\n\nError: {ex.Message}",
"Camera Connection Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
}
}
public void ClosePicturePanel()
{
if (PicturePanel.Content is PictureGalleryPage page)
page.CloseOpenDialog();
if (PicturePanel.Content is null) return;
SetVisibilityPicturePanel(false);
}
private void SetVisibilityPicturePanel(bool visible)
{
if (visible)
{
_viewModel.DismissGalleryPrompt();
PicturePanel.Navigate(new PictureGalleryPage(_appSettings, _logger, _pictureGalleryService,
_photoPrismUploadService));
_pictureGalleryService.ResetNewPhotoCount();
ButtonPanel.Visibility = Visibility.Hidden;
ActionButtonsContainer.Visibility = Visibility.Hidden;
ShutdownDock.Visibility = Visibility.Hidden;
}
else
{
PicturePanel.ClearValue(ContentProperty);
ButtonPanel.Visibility = Visibility.Visible;
ActionButtonsContainer.Visibility = Visibility.Visible;
ShutdownDock.Visibility = Visibility.Visible;
}
_isPicturePanelVisible = visible;
}
private void SetVisibilityDebugConsole(bool visible)
{
if (!_appSettings.IsDebugConsoleVisible) return;
if (visible)
DebugFrame.Navigate(new DebugConsolePage(_logger));
else
DebugFrame.ClearValue(ContentProperty);
}
private void SwitchButtonAndTimerPanel()
{
ButtonPanel.Visibility = ButtonPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
ActionButtonsContainer.Visibility = ActionButtonsContainer.Visibility == Visibility.Hidden
? Visibility.Visible
: Visibility.Hidden;
TimerPanel.Visibility = TimerPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
}
private void StartFocusStatusAnimation()
{
_focusStatusDots = 0;
CaptureStatusText.Text = "Scharfstellen";
_focusStatusAnimationTimer.Start();
}
private void StopFocusStatusAnimation()
{
_focusStatusAnimationTimer.Stop();
_focusStatusDots = 0;
}
private async Task ShowLatestPhotoDialogAsync()
{
try
{
await _pictureGalleryService.LoadThumbnailsToCache(1);
var latestPhotos = _pictureGalleryService.ThumbnailsOrderedByNewestDescending;
if (latestPhotos.Count == 0)
{
_logger.Error("No photos found in gallery");
return;
}
var latestPhoto = latestPhotos[0];
if (latestPhoto.UriSource == null)
{
_logger.Error("Latest photo UriSource is null");
return;
}
var photoPath = Uri.UnescapeDataString(latestPhoto.UriSource.AbsolutePath);
if (PicturePanel.Content is PictureGalleryPage galleryPage)
await galleryPage.ShowPhotoDialogAsync(photoPath);
}
catch (Exception ex)
{
_logger.Error($"Error showing photo dialog: {ex.Message}");
}
}
#endregion
} }

View File

@ -0,0 +1,148 @@
using System.IO;
using System.Threading.Tasks;
using System.Windows.Threading;
using CamBooth.App.Core.AppSettings;
using CamBooth.App.Core.Logging;
using CamBooth.App.Features.Camera;
using CamBooth.App.Features.PhotoPrismUpload;
using CamBooth.App.Features.PictureGallery;
namespace CamBooth.App;
public class MainWindowViewModel
{
private readonly Logger _logger;
private readonly AppSettingsService _appSettings;
private readonly PictureGalleryService _pictureGalleryService;
private readonly CameraService _cameraService;
private readonly PhotoPrismUploadService _photoPrismUploadService;
private readonly DispatcherTimer _galleryPromptTimer;
public bool IsPhotoProcessRunning { get; private set; }
// Events → MainWindow subscribes and updates UI
public event Action<string, string>? LoadingProgressChanged; // (statusText, countText)
public event Action? InitializationCompleted;
public event Action? GalleryPromptRequested;
public event Action? GalleryPromptDismissed;
public MainWindowViewModel(
Logger logger,
AppSettingsService appSettings,
PictureGalleryService pictureGalleryService,
CameraService cameraService,
PhotoPrismUploadService photoPrismUploadService)
{
_logger = logger;
_appSettings = appSettings;
_pictureGalleryService = pictureGalleryService;
_cameraService = cameraService;
_photoPrismUploadService = photoPrismUploadService;
_galleryPromptTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
_galleryPromptTimer.Tick += (_, _) => DismissGalleryPrompt();
if (appSettings.PhotoPrismAutoUploadEnabled)
_ = InitializePhotoUploadAsync();
}
public async Task InitializeAsync()
{
try
{
await LoadThumbnailsAsync();
await Task.Delay(500);
}
catch (Exception ex)
{
_logger.Error($"Initialization error: {ex.Message}");
}
finally
{
InitializationCompleted?.Invoke();
}
}
/// <summary>
/// Called by MainWindow when the countdown timer fires and a photo should be taken.
/// Throws on camera error so MainWindow can show a message box.
/// </summary>
public void OnTimerElapsed()
{
IsPhotoProcessRunning = false;
_cameraService.TakePhoto();
ShowGalleryPrompt();
}
public void StartPhotoProcess()
{
IsPhotoProcessRunning = true;
DismissGalleryPrompt();
}
public void CancelPhotoProcess()
{
IsPhotoProcessRunning = false;
}
public void ShowGalleryPrompt()
{
GalleryPromptRequested?.Invoke();
_galleryPromptTimer.Stop();
_galleryPromptTimer.Start();
}
public void DismissGalleryPrompt()
{
_galleryPromptTimer.Stop();
GalleryPromptDismissed?.Invoke();
}
private async Task LoadThumbnailsAsync()
{
var pictureLocation = _appSettings.PictureLocation;
if (!Directory.Exists(pictureLocation))
{
LoadingProgressChanged?.Invoke("Keine Fotos gefunden", "0 Fotos");
await Task.Delay(1000);
return;
}
string[] imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif"];
var totalCount = Directory.EnumerateFiles(pictureLocation!)
.Count(f => imageExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()));
if (totalCount == 0)
{
LoadingProgressChanged?.Invoke("Keine Fotos gefunden", "Bereit für neue Aufnahmen!");
await Task.Delay(1000);
return;
}
var photoLabel = totalCount == 1 ? "Foto" : "Fotos";
LoadingProgressChanged?.Invoke($"Lade {totalCount} {photoLabel}...", $"0 / {totalCount}");
await _pictureGalleryService.LoadThumbnailsToCache();
LoadingProgressChanged?.Invoke("Fotos erfolgreich geladen!", $"{totalCount} {photoLabel} bereit");
}
private async Task InitializePhotoUploadAsync()
{
_logger.Info("PhotoPrism Auto-Upload aktiviert. Authentifiziere...");
var authSuccess = await _photoPrismUploadService.AuthenticateAsync();
if (authSuccess)
_logger.Info("PhotoPrism-Authentifizierung erfolgreich!");
else
_logger.Warning("PhotoPrism-Authentifizierung fehlgeschlagen.");
}
}