From b3c91da331c6015d9d671d62665d57c10982644a Mon Sep 17 00:00:00 2001 From: iTob Date: Mon, 9 Mar 2026 22:24:39 +0100 Subject: [PATCH] Refactor: Simplify `CameraService`, improve session handling, and streamline application initialization --- src/CamBooth/CamBooth.App/App.xaml.cs | 228 ++-- .../Features/Camera/CameraService.cs | 699 +++++------- .../Features/LiveView/LiveViewPage.xaml.cs | 143 ++- src/CamBooth/CamBooth.App/MainWindow.xaml.cs | 999 +++++++----------- .../CamBooth.App/MainWindowViewModel.cs | 148 +++ 5 files changed, 975 insertions(+), 1242 deletions(-) create mode 100644 src/CamBooth/CamBooth.App/MainWindowViewModel.cs diff --git a/src/CamBooth/CamBooth.App/App.xaml.cs b/src/CamBooth/CamBooth.App/App.xaml.cs index da5b1d6..efb837f 100644 --- a/src/CamBooth/CamBooth.App/App.xaml.cs +++ b/src/CamBooth/CamBooth.App/App.xaml.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Windows; using CamBooth.App.Core.AppSettings; @@ -21,134 +20,113 @@ namespace CamBooth.App; /// public partial class App : Application { - private IServiceProvider _serviceProvider; +private IServiceProvider? _serviceProvider; - protected override void OnStartup(StartupEventArgs e) - { - base.OnStartup(e); +protected override void OnStartup(StartupEventArgs e) +{ +base.OnStartup(e); - // Konfiguration laden - var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"; - - var configBuilder = new ConfigurationBuilder() - .SetBasePath(AppContext.BaseDirectory) - .AddJsonFile("Core/AppSettings/app.settings.json", optional: false, reloadOnChange: true); +var configuration = BuildConfiguration(); +var services = new ServiceCollection(); - if (environment == "Development") - { - configBuilder.AddJsonFile("Core/AppSettings/app.settings.dev.json", optional: true, reloadOnChange: true); - } +RegisterServices(services, configuration); - var configuration = configBuilder.Build(); +_serviceProvider = services.BuildServiceProvider(); - var services = new ServiceCollection(); - - services.AddSingleton(configuration); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - var tempProvider = services.BuildServiceProvider(); - var appSettings = tempProvider.GetRequiredService(); - var logger = tempProvider.GetRequiredService(); +StartBackgroundServices(); - 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(); - } - else - { - services.AddSingleton(); - } - } - 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(); - } - - services.AddTransient(); - - _serviceProvider = services.BuildServiceProvider(); - - // Starte PhotoPrism Upload-Service beim Start - try - { - var uploadQueueService = _serviceProvider.GetRequiredService(); - uploadQueueService.Start(); - - // Scan für fehlgeschlagene Uploads beim Start - 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.Show(); - } - - protected override void OnExit(ExitEventArgs e) - { - // Stoppe PhotoPrism 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 PhotoPrism UploadQueueService: {ex.Message}"); - } - - // Dispose Service Provider, damit IDisposable-Services (z.B. CameraService) sauber beendet werden. - try - { - if (_serviceProvider is IDisposable disposableProvider) - { - disposableProvider.Dispose(); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"Fehler beim Dispose des ServiceProviders: {ex.Message}"); - } - - base.OnExit(e); - } +_serviceProvider.GetRequiredService().Show(); +} + +protected override void OnExit(ExitEventArgs e) +{ +try +{ +_serviceProvider?.GetService() +?.StopAsync().Wait(TimeSpan.FromSeconds(10)); +} +catch (Exception ex) +{ +System.Diagnostics.Debug.WriteLine($"Error stopping upload service: {ex.Message}"); +} + +try +{ +(_serviceProvider as IDisposable)?.Dispose(); +} +catch (Exception ex) +{ +System.Diagnostics.Debug.WriteLine($"Error disposing service provider: {ex.Message}"); +} + +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(); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +services.AddTransient(); + +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(new CanonAPIMock()); +} +} + + +private void StartBackgroundServices() +{ +try +{ +var logger = _serviceProvider!.GetRequiredService(); +var uploadQueueService = _serviceProvider.GetRequiredService(); +uploadQueueService.Start(); +uploadQueueService.ScanAndQueueFailedUploads(); +logger.Info("PhotoPrism UploadQueueService gestartet"); +} +catch (Exception ex) +{ +System.Diagnostics.Debug.WriteLine($"Error starting background services: {ex.Message}"); +} +} } diff --git a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs index 1198f76..fc83eb5 100644 --- a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs +++ b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs @@ -14,425 +14,284 @@ namespace CamBooth.App.Features.Camera; public class CameraService : IDisposable { - private readonly AppSettingsService _appSettings; - - private readonly Logger _logger; - - private readonly PictureGalleryService _pictureGalleryService; - - private readonly PhotoPrismUploadQueueService _photoPrismUploadQueueService; - - private readonly ICanonAPI _APIHandler; - - private CameraValue[] AvList; - - private int BulbTime = 30; - - private List CamList; - - private int ErrCount; - - private object ErrLock = new(); - - private bool IsInit; - - private CameraValue[] ISOList; - - public ICamera? _mainCamera; - - private CameraValue[] TvList; - - - public CameraService(Logger logger, - AppSettingsService appSettings, - PictureGalleryService pictureGalleryService, - PhotoPrismUploadQueueService photoPrismUploadQueueService, - ICanonAPI APIHandler) - { - this._logger = logger; - this._appSettings = appSettings; - this._pictureGalleryService = pictureGalleryService; - this._photoPrismUploadQueueService = photoPrismUploadQueueService; - this._APIHandler = APIHandler; - try - { - this.IsInit = true; - } - catch (DllNotFoundException) - { - this.ReportError("Canon DLLs not found!"); - } - catch (Exception ex) - { - this.ReportError(ex.Message); - } - } - - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() - { - this.CloseSession(); - this.IsInit = false; - this._APIHandler.Dispose(); - this._mainCamera?.Dispose(); - } - - - 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 = 750; // milliseconds - - for (int attempt = 0; attempt < maxRetries; attempt++) - { - if (this.CamList != null && this.CamList.Any()) - { - break; - } - - if (attempt < maxRetries - 1) - { - 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"); - } - - string cameraDeviceNames = string.Join(", ", this.CamList.Select(cam => cam.DeviceName)); - this._logger.Debug(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) - { - this._logger.Error($"Error connecting camera: {ex.Message}"); - throw; - } - } - - - private void SetSettingSaveToComputer() - { - this._mainCamera.SetSetting(PropertyID.SaveTo, (int)SaveTo.Host); - this._mainCamera.SetCapacity(4096, int.MaxValue); - } - - - public void CloseSession() - { - try - { - if (this._mainCamera != null && this._mainCamera.SessionOpen) - { - this._mainCamera.CloseSession(); - this._logger.Info("Camera session closed"); - } - } - catch (Exception ex) - { - this._logger.Error($"Error closing camera session: {ex.Message}"); - } - - // AvCoBox.Items.Clear(); - // 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() - { - try - { - this._mainCamera.TakePhoto(); - } - catch (Exception ex) - { - this.ReportError(ex.Message); - throw; - } - } - public async Task PrepareFocusAsync(int focusTimeoutMs = 1500) - { - if (this._mainCamera is not EOSDigital.API.Camera sdkCamera) - { - await Task.Delay(200); - return; - } - - var focusCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - void FocusStateChanged(EOSDigital.API.Camera sender, StateEventID eventId, int parameter) - { - if (eventId == StateEventID.AfResult) - { - focusCompleted.TrySetResult(true); - } - } - - sdkCamera.StateChanged += FocusStateChanged; - - try - { - await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.Halfway)); - var completedTask = await Task.WhenAny(focusCompleted.Task, Task.Delay(focusTimeoutMs)); - if (completedTask != focusCompleted.Task) - { - this._logger.Info("Autofocus timeout reached, continuing with countdown."); - } - } - catch (Exception ex) - { - this.ReportError(ex.Message); - } - finally - { - try - { - await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.OFF)); - } - catch (Exception ex) - { - this.ReportError(ex.Message); - } - - sdkCamera.StateChanged -= FocusStateChanged; - } - } - - - #region API Events - - // private void APIHandler_CameraAdded(CanonAPI sender) - // { - // try - // { - // } - // catch (Exception ex) - // { - // ReportError(ex.Message, false); - // } - // } - - - private void MainCamera_StateChanged(EOSDigital.API.Camera sender, StateEventID eventID, int parameter) - { - try - { - if (eventID == StateEventID.Shutdown && this.IsInit) - { - Application.Current.Dispatcher.Invoke(() => this.CloseSession()); - - //Dispatcher.Invoke((Action)delegate { CloseSession(); }); - } - } - catch (Exception ex) - { - this.ReportError(ex.Message); - } - } - - - // private void MainCamera_ProgressChanged(object sender, int progress) - // { - // 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"); - try - { - Info.FileName = $"img_{Guid.NewGuid().ToString()}.jpg"; - sender.DownloadFile(Info, this._appSettings.PictureLocation); - var savedPhotoPath = Path.Combine(this._appSettings.PictureLocation, Info.FileName); - this._logger.Info("Download complete: " + savedPhotoPath); - - Application.Current.Dispatcher.Invoke(() => { - this._pictureGalleryService.IncrementNewPhotoCount(); - this._pictureGalleryService.LoadThumbnailsToCache(); - }); - - // Füge neues Foto zur PhotoPrism Upload-Queue hinzu (wenn Auto-Upload aktiviert) - this._photoPrismUploadQueueService.QueueNewPhoto(savedPhotoPath); - this._logger.Info($"Foto zur PhotoPrism Upload-Queue hinzugefügt: {Info.FileName}"); - } - catch (Exception ex) - { - this.ReportError(ex.Message); - } - } - - - private void ErrorHandler_NonSevereErrorHappened(object sender, ErrorCode ex) - { - this.ReportError($"SDK Error code: {ex} ({((int)ex).ToString("X")})"); - } - - - private void ErrorHandler_SevereErrorHappened(object sender, Exception ex) - { - this.ReportError(ex.Message); - } - - #endregion +private readonly AppSettingsService _appSettings; +private readonly Logger _logger; +private readonly PictureGalleryService _pictureGalleryService; +private readonly PhotoPrismUploadQueueService _photoPrismUploadQueueService; +private readonly ICanonAPI _canonApi; + +private ICamera? _mainCamera; +private List? _camList; +private bool _isConnected; + +/// Fires whenever the camera delivers a new live-view frame. +public event Action? LiveViewUpdated; + +public bool IsConnected => _isConnected && _mainCamera?.SessionOpen == true; + + +public CameraService( +Logger logger, +AppSettingsService appSettings, +PictureGalleryService pictureGalleryService, +PhotoPrismUploadQueueService photoPrismUploadQueueService, +ICanonAPI canonApi) +{ +_logger = logger; +_appSettings = appSettings; +_pictureGalleryService = pictureGalleryService; +_photoPrismUploadQueueService = photoPrismUploadQueueService; +_canonApi = canonApi; } + +public void Dispose() +{ +CloseSession(); +_canonApi.Dispose(); +_mainCamera?.Dispose(); +} + + +public void ConnectCamera() +{ +ErrorHandler.SevereErrorHappened += ErrorHandler_SevereErrorHappened; +ErrorHandler.NonSevereErrorHappened += ErrorHandler_NonSevereErrorHappened; + +try +{ +RefreshCameraList(); + +const int maxRetries = 3; +const int retryDelayMs = 750; + +for (int attempt = 0; attempt < maxRetries; attempt++) +{ +if (_camList?.Any() == true) break; + +if (attempt < maxRetries - 1) +{ +System.Threading.Thread.Sleep(retryDelayMs); +RefreshCameraList(); +} +} + +if (_camList?.Any() != true) +throw new InvalidOperationException("No cameras found after multiple attempts."); + +_mainCamera = _camList[0]; +_logger.Info($"Camera found: {_mainCamera.DeviceName}"); + +OpenSession(); +SetSaveToComputer(); +StartLiveView(); +_isConnected = true; +} +catch (Exception ex) +{ +_logger.Error($"Error connecting camera: {ex.Message}"); +throw; +} +} + + +public void CloseSession() +{ +try +{ +if (_mainCamera?.SessionOpen == true) +{ +_mainCamera.CloseSession(); +_logger.Info("Camera session closed"); +} +} +catch (Exception ex) +{ +_logger.Error($"Error closing camera session: {ex.Message}"); +} + +_isConnected = false; +} + + +public void TakePhoto() +{ +if (_mainCamera == null) throw new InvalidOperationException("Camera not connected."); +_mainCamera.TakePhoto(); +} + + +public async Task PrepareFocusAsync(int focusTimeoutMs = 1500) +{ +if (_mainCamera is not EOSDigital.API.Camera sdkCamera) +{ +await Task.Delay(200); +return; +} + +var focusCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + +void FocusStateChanged(EOSDigital.API.Camera sender, StateEventID eventId, int parameter) +{ +if (eventId == StateEventID.AfResult) +focusCompleted.TrySetResult(true); +} + +sdkCamera.StateChanged += FocusStateChanged; + +try +{ +await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.Halfway)); +var completed = await Task.WhenAny(focusCompleted.Task, Task.Delay(focusTimeoutMs)); +if (completed != focusCompleted.Task) +_logger.Info("Autofocus timeout reached, continuing."); +} +catch (Exception ex) +{ +_logger.Error(ex.Message); +} +finally +{ +try +{ +await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.OFF)); +} +catch (Exception ex) { _logger.Error(ex.Message); } + +sdkCamera.StateChanged -= FocusStateChanged; +} +} + + +private void RefreshCameraList() => _camList = _canonApi.GetCameraList(); + + +private void OpenSession() +{ +if (_mainCamera == null) +throw new InvalidOperationException("Camera reference is null."); + +if (_mainCamera.SessionOpen) +{ +_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) +{ +try +{ +if (eventID == StateEventID.Shutdown && _isConnected) +Application.Current.Dispatcher.Invoke(CloseSession); +} +catch (Exception ex) +{ +_logger.Error(ex.Message); +} +} + + +private void MainCamera_DownloadReady(ICamera sender, IDownloadInfo info) +{ +_logger.Info("Download ready"); +try +{ +info.FileName = $"img_{Guid.NewGuid()}.jpg"; +sender.DownloadFile(info, _appSettings.PictureLocation); +var savedPath = Path.Combine(_appSettings.PictureLocation!, info.FileName); +_logger.Info($"Download complete: {savedPath}"); + +Application.Current.Dispatcher.Invoke(() => +{ +_pictureGalleryService.IncrementNewPhotoCount(); +_pictureGalleryService.LoadThumbnailsToCache(); +}); + +_photoPrismUploadQueueService.QueueNewPhoto(savedPath); +} +catch (Exception ex) +{ +_logger.Error(ex.Message); +} +} + + +private void ErrorHandler_NonSevereErrorHappened(object sender, ErrorCode ex) => +_logger.Error($"SDK Error: {ex} (0x{(int)ex:X})"); + + +private void ErrorHandler_SevereErrorHappened(object sender, Exception ex) => +_logger.Error(ex.Message); + +#endregion +} diff --git a/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs b/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs index 5e60f0b..c677e17 100644 --- a/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs +++ b/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs @@ -1,10 +1,9 @@ -using System.IO; +using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; -using CamBooth.App.Core.AppSettings; using CamBooth.App.Core.Logging; using CamBooth.App.Features.Camera; @@ -12,87 +11,69 @@ using EOSDigital.API; 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 Logger _logger; - - private readonly ImageBrush bgbrush = new(); - - private readonly Action SetImageAction; +private readonly CameraService _cameraService; +private readonly Logger _logger; +private readonly ImageBrush _bgBrush = new(); - public LiveViewPage(Logger logger, AppSettingsService appSettings, CameraService cameraService) - { - this._logger = logger; - this._appSettings = appSettings; - this._cameraService = cameraService; - this.InitializeComponent(); - this.SetImageAction = img => { this.bgbrush.ImageSource = img; }; - - // Configure the image brush - this.bgbrush.Stretch = Stretch.UniformToFill; - this.bgbrush.AlignmentX = AlignmentX.Center; - this.bgbrush.AlignmentY = AlignmentY.Center; - - this.LVCanvas.Background = this.bgbrush; - - // Apply horizontal flip on the Canvas using RenderTransform - TransformGroup transformGroup = new(); - transformGroup.Children.Add(new ScaleTransform { ScaleX = -1, ScaleY = 1 }); - transformGroup.Children.Add(new TranslateTransform { X = 1, Y = 0 }); - this.LVCanvas.RenderTransform = transformGroup; - this.LVCanvas.RenderTransformOrigin = new Point(0.5, 0.5); - - try - { - cameraService.ConnectCamera(); - - // Verify that camera session is open before subscribing to events - 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) - { - this._logger.Error($"Failed to initialize LiveViewPage: {ex.Message}"); - throw; - } - } +public LiveViewPage(Logger logger, CameraService cameraService) +{ +_logger = logger; +_cameraService = cameraService; + +InitializeComponent(); + +_bgBrush.Stretch = Stretch.UniformToFill; +_bgBrush.AlignmentX = AlignmentX.Center; +_bgBrush.AlignmentY = AlignmentY.Center; +LVCanvas.Background = _bgBrush; + +var transformGroup = new TransformGroup(); +transformGroup.Children.Add(new ScaleTransform { ScaleX = -1, ScaleY = 1 }); +transformGroup.Children.Add(new TranslateTransform { X = 1, Y = 0 }); +LVCanvas.RenderTransform = transformGroup; +LVCanvas.RenderTransformOrigin = new Point(0.5, 0.5); + +try +{ +cameraService.ConnectCamera(); +cameraService.LiveViewUpdated += OnLiveViewUpdated; +_logger.Info("LiveViewPage initialized successfully"); +} +catch (Exception ex) +{ +_logger.Error($"Failed to initialize LiveViewPage: {ex.Message}"); +throw; +} +} - private void MainCamera_OnLiveViewUpdated(ICamera sender, Stream img) - { - try - { - using WrapStream s = new(img); - img.Position = 0; - BitmapImage EvfImage = new(); - EvfImage.BeginInit(); - EvfImage.StreamSource = s; - EvfImage.CacheOption = BitmapCacheOption.OnLoad; - EvfImage.EndInit(); - EvfImage.Freeze(); - Application.Current.Dispatcher.BeginInvoke(this.SetImageAction, EvfImage); - } - catch (Exception ex) - { - this._logger.Error(ex.Message); - } - } - - public void Dispose() - { - this._cameraService.Dispose(); - } -} \ No newline at end of file +public void Dispose() +{ +_cameraService.LiveViewUpdated -= OnLiveViewUpdated; +_cameraService.Dispose(); +} + + +private void OnLiveViewUpdated(Stream img) +{ +try +{ +using WrapStream s = new(img); +img.Position = 0; +var evfImage = new BitmapImage(); +evfImage.BeginInit(); +evfImage.StreamSource = s; +evfImage.CacheOption = BitmapCacheOption.OnLoad; +evfImage.EndInit(); +evfImage.Freeze(); +Application.Current.Dispatcher.BeginInvoke(() => _bgBrush.ImageSource = evfImage); +} +catch (Exception ex) +{ +_logger.Error(ex.Message); +} +} +} diff --git a/src/CamBooth/CamBooth.App/MainWindow.xaml.cs b/src/CamBooth/CamBooth.App/MainWindow.xaml.cs index e19b12f..d48fedc 100644 --- a/src/CamBooth/CamBooth.App/MainWindow.xaml.cs +++ b/src/CamBooth/CamBooth.App/MainWindow.xaml.cs @@ -1,14 +1,11 @@ -using System.ComponentModel; +using System.ComponentModel; using System.Diagnostics; -using System.IO; using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; - using CamBooth.App.Core.AppSettings; using CamBooth.App.Core.Logging; using CamBooth.App.Features.Camera; @@ -16,7 +13,6 @@ using CamBooth.App.Features.DebugConsole; using CamBooth.App.Features.LiveView; using CamBooth.App.Features.PhotoPrismUpload; using CamBooth.App.Features.PictureGallery; - using Wpf.Ui.Controls; namespace CamBooth.App; @@ -26,614 +22,385 @@ namespace CamBooth.App; /// public partial class MainWindow : Window { - private readonly Logger _logger; - - private readonly AppSettingsService _appSettings; - - private readonly PictureGalleryService _pictureGalleryService; - - private readonly CameraService _cameraService; - - private readonly PhotoPrismUploadService _photoPrismUploadService; - - private bool _isDebugConsoleVisible = true; - - private bool _isPicturePanelVisible = false; - - private LiveViewPage? _liveViewPage; - - private bool _isPhotoProcessRunning; - - private bool _isCameraStarted; - - private bool _isShutdownSliderOpen; - - private const string ShutdownGlyphClosed = "\uE7E8"; - - private const string ShutdownGlyphOpen = "\uE711"; - - private const double ShutdownSliderOffset = 160; - - private readonly DispatcherTimer _focusStatusAnimationTimer = new() { Interval = TimeSpan.FromMilliseconds(250) }; - - private readonly DispatcherTimer _galleryPromptTimer = new() { Interval = TimeSpan.FromSeconds(5) }; - - private int _focusStatusDots; - - - public MainWindow( - Logger logger, - AppSettingsService appSettings, - PictureGalleryService pictureGalleryService, - CameraService cameraService, - PhotoPrismUploadService photoPrismUploadService) - { - this._logger = logger; - this._appSettings = appSettings; - this._pictureGalleryService = pictureGalleryService; - this._cameraService = cameraService; - this._photoPrismUploadService = photoPrismUploadService; - InitializeComponent(); - this.SetVisibilityDebugConsole(_appSettings.IsDebugConsoleVisible); - this.SetVisibilityPicturePanel(this._isPicturePanelVisible); - - // Lade Thumbnails asynchron und zeige dann den Welcome Screen - _ = InitializeAsync(); - - this.Closing += OnClosing; - TimerControlRectangleAnimation.OnTimerEllapsed += TimerControlRectangleAnimation_OnTimerEllapsed; - this._focusStatusAnimationTimer.Tick += (_, _) => - { - this._focusStatusDots = (this._focusStatusDots + 1) % 4; - this.CaptureStatusText.Text = $"Scharfstellen{new string('.', this._focusStatusDots)}"; - }; - this._galleryPromptTimer.Tick += (_, _) => this.HideGalleryPrompt(); - - // Subscribe to new photo count changes - this._pictureGalleryService.NewPhotoCountChanged += PictureGalleryService_NewPhotoCountChanged; - - this.DebugCloseButton.Visibility = Visibility.Collapsed; - this.HideDebugButton.Visibility = this._appSettings.IsDebugConsoleVisible ? Visibility.Visible : Visibility.Collapsed; - - // Initialize PhotoPrism upload if auto-upload is enabled - if (appSettings.PhotoPrismAutoUploadEnabled) - { - logger.Info("PhotoPrism Auto-Upload ist aktiviert. Authentifiziere..."); - _ = Task.Run(async () => - { - var authSuccess = await _photoPrismUploadService.AuthenticateAsync(); - if (authSuccess) - { - logger.Info("PhotoPrism-Authentifizierung erfolgreich!"); - } - else - { - 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() - { - 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() - { - 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."); - this._logger.Info(exception.Message); - } - finally - { - this.StopFocusStatusAnimation(); - this.CaptureStatusText.Visibility = Visibility.Collapsed; - SwitchButtonAndTimerPanel(); - this._isPhotoProcessRunning = false; - if (photoTakenSuccessfully) - { - this.ShowGalleryPrompt(); - } - } - } - - - 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) - { - 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 - { - this._liveViewPage = new LiveViewPage(this._logger, this._appSettings, this._cameraService); - this.MainFrame.Navigate(this._liveViewPage); - this._isCameraStarted = true; - } - catch (Exception ex) - { - this._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); - } - } - - - private void SetVisibilityDebugConsole(object sender, RoutedEventArgs e) - { - this.SetVisibilityDebugConsole(this._isDebugConsoleVisible); - } - - - 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) - { - this.HideGalleryPrompt(); - this.ClosePicturePanel(); - - if (this._isPhotoProcessRunning) - { - return; - } - - this._isPhotoProcessRunning = true; - - try - { - SwitchButtonAndTimerPanel(); - - TimerControlRectangleAnimation.StartTimer(this._appSettings.PhotoCountdownSeconds); - this.StartFocusStatusAnimation(); - 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); - this.StopFocusStatusAnimation(); - this.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) - { - this._logger.Error($"Fehler beim Schließen der Kamera-Session: {ex.Message}"); - } - } - - private void ToggleShutdownSlider(object sender, RoutedEventArgs e) - { - this._isShutdownSliderOpen = !this._isShutdownSliderOpen; - this.ShutdownToggleButton.Content = this._isShutdownSliderOpen ? ShutdownGlyphOpen : ShutdownGlyphClosed; - this.AnimateShutdownSlider(this._isShutdownSliderOpen); - } - - private void AnimateShutdownSlider(bool open) - { - var animation = new DoubleAnimation - { - To = open ? 0 : ShutdownSliderOffset, - Duration = TimeSpan.FromMilliseconds(250), - EasingFunction = new QuadraticEase() - }; - - this.ShutdownSliderTransform.BeginAnimation(System.Windows.Media.TranslateTransform.XProperty, animation); - } - - private void StartFocusStatusAnimation() - { - this._focusStatusDots = 0; - this.CaptureStatusText.Text = "Scharfstellen"; - this._focusStatusAnimationTimer.Start(); - } - - private void StopFocusStatusAnimation() - { - this._focusStatusAnimationTimer.Stop(); - this._focusStatusDots = 0; - } - - private void ShowGalleryPrompt() - { - 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 - { - // Get the latest photo path - await this._pictureGalleryService.LoadThumbnailsToCache(1); - var latestPhotos = this._pictureGalleryService.ThumbnailsOrderedByNewestDescending; - - if (latestPhotos.Count == 0) - { - this._logger.Error("No photos found in gallery"); - return; - } - - // Get the file path from the BitmapImage source - var latestPhoto = latestPhotos[0]; - if (latestPhoto.UriSource == null) - { - this._logger.Error("Latest photo UriSource is null"); - return; - } - - string photoPath = Uri.UnescapeDataString(latestPhoto.UriSource.AbsolutePath); - this._logger.Info($"Opening photo dialog for: {photoPath}"); - - // Get the current gallery page - if (this.PicturePanel.Content is PictureGalleryPage galleryPage) - { - // Show the photo dialog - await galleryPage.ShowPhotoDialogAsync(photoPath); - } - else - { - this._logger.Error("PicturePanel content is not a PictureGalleryPage"); - } - } - catch (Exception ex) - { - this._logger.Error($"Error showing photo dialog: {ex.Message}"); - } - } - - 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; - } - } - - private void ShowQRCode(object sender, MouseButtonEventArgs e) - { - try - { - this._logger.Info("Zeige PhotoPrism Album-QR-Code an..."); - - // Generiere QR-Code - var qrCodeImage = this._photoPrismUploadService.GenerateAlbumQRCode(); - - if (qrCodeImage == null) - { - 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); - return; - } - - // Öffne QR-Code-Fenster - var qrWindow = new PhotoPrismQRCodeDisplayWindow(); - qrWindow.SetQRCode(qrCodeImage); - qrWindow.ShowDialog(); - } - catch (Exception ex) - { - this._logger.Error($"Fehler beim Anzeigen des PhotoPrism 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); - } - } -} + private readonly Logger _logger; + private readonly AppSettingsService _appSettings; + private readonly PictureGalleryService _pictureGalleryService; + private readonly CameraService _cameraService; + private readonly PhotoPrismUploadService _photoPrismUploadService; + private readonly MainWindowViewModel _viewModel; + + private LiveViewPage? _liveViewPage; + private bool _isCameraStarted; + private bool _isPicturePanelVisible; + private bool _isDebugConsoleVisible; + private bool _isShutdownSliderOpen; + + private const string ShutdownGlyphClosed = "\uE7E8"; + private const string ShutdownGlyphOpen = "\uE711"; + private const double ShutdownSliderOffset = 160; + + private readonly DispatcherTimer _focusStatusAnimationTimer = new() { Interval = TimeSpan.FromMilliseconds(250) }; + private int _focusStatusDots; + + + public MainWindow( + Logger logger, + AppSettingsService appSettings, + PictureGalleryService pictureGalleryService, + CameraService cameraService, + PhotoPrismUploadService photoPrismUploadService, + MainWindowViewModel viewModel) + { + _logger = logger; + _appSettings = appSettings; + _pictureGalleryService = pictureGalleryService; + _cameraService = cameraService; + _photoPrismUploadService = photoPrismUploadService; + _viewModel = viewModel; + + InitializeComponent(); + +// Wire ViewModel events to UI + _viewModel.LoadingProgressChanged += (status, count) => + { + LoadingStatusText.Text = status; + LoadingCountText.Text = count; + }; + _viewModel.InitializationCompleted += () => + { + LoadingOverlay.Visibility = Visibility.Collapsed; + WelcomeOverlay.Visibility = Visibility.Visible; + }; + _viewModel.GalleryPromptRequested += () => GalleryPrompt.Visibility = Visibility.Visible; + _viewModel.GalleryPromptDismissed += () => GalleryPrompt.Visibility = Visibility.Collapsed; + +// Wire service events + _pictureGalleryService.NewPhotoCountChanged += OnNewPhotoCountChanged; + TimerControlRectangleAnimation.OnTimerEllapsed += OnTimerElapsed; + +// Focus animation timer + _focusStatusAnimationTimer.Tick += (_, _) => + { + _focusStatusDots = (_focusStatusDots + 1) % 4; + CaptureStatusText.Text = $"Scharfstellen{new string('.', _focusStatusDots)}"; + }; + +// Initial UI state + _isDebugConsoleVisible = _appSettings.IsDebugConsoleVisible; + SetVisibilityDebugConsole(_isDebugConsoleVisible); + SetVisibilityPicturePanel(false); + DebugCloseButton.Visibility = Visibility.Collapsed; + HideDebugButton.Visibility = _appSettings.IsDebugConsoleVisible ? Visibility.Visible : Visibility.Collapsed; + + Closing += OnClosing; + + _ = _viewModel.InitializeAsync(); + _logger.Info($"config file loaded: '{appSettings.ConfigFileName}'"); + _logger.Info("MainWindow initialized"); + } + + + #region Event handlers (wired in XAML or in ctor) + + private void OnTimerElapsed() + { + try + { + _viewModel.OnTimerElapsed(); + } + catch (Exception ex) + { + _logger.Error(ex.Message); + System.Windows.MessageBox.Show("Sorry, da ging was schief! Bitte nochmal probieren."); + } + finally + { + StopFocusStatusAnimation(); + CaptureStatusText.Visibility = Visibility.Collapsed; + SwitchButtonAndTimerPanel(); + } + } + + private void OnNewPhotoCountChanged(object? sender, int count) + { + NewPhotosBadge.Visibility = count > 0 ? Visibility.Visible : Visibility.Collapsed; + if (count > 0) + NewPhotoCountText.Text = count.ToString(); + } + + private void OnClosing(object? sender, CancelEventArgs e) + { + try + { + _cameraService.CloseSession(); + } + catch (Exception ex) + { + _logger.Error($"Fehler beim Schließen der Kamera-Session: {ex.Message}"); + } + + _liveViewPage?.Dispose(); + } + +// XAML-bound handlers + private void StartExperience(object sender, RoutedEventArgs e) + { + StartLiveViewIfNeeded(); + WelcomeOverlay.Visibility = Visibility.Collapsed; + ButtonPanel.Visibility = Visibility.Visible; + ActionButtonsContainer.Visibility = Visibility.Visible; + ShutdownDock.Visibility = Visibility.Visible; + } + + private async void StartTakePhotoProcess(object sender, RoutedEventArgs e) + { + if (_viewModel.IsPhotoProcessRunning) return; + + ClosePicturePanel(); + _viewModel.StartPhotoProcess(); + + try + { + SwitchButtonAndTimerPanel(); + TimerControlRectangleAnimation.StartTimer(_appSettings.PhotoCountdownSeconds); + StartFocusStatusAnimation(); + CaptureStatusText.Visibility = Visibility.Visible; + + await Task.Delay(TimeSpan.FromSeconds(_appSettings.FocusDelaySeconds)); + if (!_viewModel.IsPhotoProcessRunning) return; + + await _cameraService.PrepareFocusAsync(_appSettings.FocusTimeoutMs); + StopFocusStatusAnimation(); + CaptureStatusText.Visibility = Visibility.Collapsed; + } + catch (Exception ex) + { + _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) + { + _isShutdownSliderOpen = !_isShutdownSliderOpen; + ShutdownToggleButton.Content = _isShutdownSliderOpen ? ShutdownGlyphOpen : ShutdownGlyphClosed; + var animation = new DoubleAnimation + { + To = _isShutdownSliderOpen ? 0 : ShutdownSliderOffset, + Duration = TimeSpan.FromMilliseconds(250), + EasingFunction = new QuadraticEase() + }; + ShutdownSliderTransform.BeginAnimation(System.Windows.Media.TranslateTransform.XProperty, animation); + } + + private async void ShutdownWindows(object sender, RoutedEventArgs e) + { + var confirmDialog = new ContentDialog(DialogPresenter) + { + Title = "Sicherheitsabfrage", + 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) + }; + + if (await confirmDialog.ShowAsync() != ContentDialogResult.Primary) return; + + try + { + _cameraService.CloseSession(); + } + catch + { + } + + var (args, errorMsg) = _appSettings.IsShutdownEnabled + ? ("/s /t 0", "Windows konnte nicht heruntergefahren werden.") + : ("/l", "Abmeldung fehlgeschlagen."); + + try + { + Process.Start(new ProcessStartInfo + { + FileName = "shutdown", + Arguments = args, + CreateNoWindow = true, + UseShellExecute = false + }); + } + catch (Exception ex) + { + _logger.Error(ex.Message); + System.Windows.MessageBox.Show(errorMsg); + } + } + + private void ShowQRCode(object sender, MouseButtonEventArgs e) + { + try + { + var qrCodeImage = _photoPrismUploadService.GenerateAlbumQRCode(); + if (qrCodeImage == null) + { + 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); + return; + } + + var qrWindow = new PhotoPrismQRCodeDisplayWindow(); + qrWindow.SetQRCode(qrCodeImage); + qrWindow.ShowDialog(); + } + catch (Exception ex) + { + _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); + } + } + + 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 +} \ No newline at end of file diff --git a/src/CamBooth/CamBooth.App/MainWindowViewModel.cs b/src/CamBooth/CamBooth.App/MainWindowViewModel.cs new file mode 100644 index 0000000..76c3070 --- /dev/null +++ b/src/CamBooth/CamBooth.App/MainWindowViewModel.cs @@ -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? 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(); + } + } + + + /// + /// 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. + /// + 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."); + } +}