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 CamBooth.App.Core.AppSettings;
@ -21,134 +20,113 @@ namespace CamBooth.App;
/// </summary>
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 configuration = BuildConfiguration();
var services = new ServiceCollection();
var configBuilder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("Core/AppSettings/app.settings.json", optional: false, reloadOnChange: true);
RegisterServices(services, configuration);
if (environment == "Development")
{
configBuilder.AddJsonFile("Core/AppSettings/app.settings.dev.json", optional: true, reloadOnChange: true);
}
_serviceProvider = services.BuildServiceProvider();
var configuration = configBuilder.Build();
StartBackgroundServices();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(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();
// Starte PhotoPrism Upload-Service beim Start
try
{
var uploadQueueService = _serviceProvider.GetRequiredService<PhotoPrismUploadQueueService>();
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>();
mainWindow.Show();
}
protected override void OnExit(ExitEventArgs e)
{
// Stoppe PhotoPrism UploadQueueService beim Beenden der App
try
{
var uploadQueueService = _serviceProvider?.GetService<PhotoPrismUploadQueueService>();
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<MainWindow>().Show();
}
protected override void OnExit(ExitEventArgs e)
{
try
{
_serviceProvider?.GetService<PhotoPrismUploadQueueService>()
?.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<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

@ -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<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,
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);
}
}
/// <summary> Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. </summary>
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<bool>(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<ICamera>? _camList;
private bool _isConnected;
/// <summary>Fires whenever the camera delivers a new live-view frame.</summary>
public event Action<Stream>? 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<bool>(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
}

View File

@ -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<BitmapImage> 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; };
public LiveViewPage(Logger logger, CameraService cameraService)
{
_logger = logger;
_cameraService = cameraService;
// Configure the image brush
this.bgbrush.Stretch = Stretch.UniformToFill;
this.bgbrush.AlignmentX = AlignmentX.Center;
this.bgbrush.AlignmentY = AlignmentY.Center;
InitializeComponent();
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
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);
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();
// 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;
}
}
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();
}
try
{
cameraService.ConnectCamera();
cameraService.LiveViewUpdated += OnLiveViewUpdated;
_logger.Info("LiveViewPage initialized successfully");
}
catch (Exception ex)
{
_logger.Error($"Failed to initialize LiveViewPage: {ex.Message}");
throw;
}
}
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);
}
}
}

View File

@ -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;
@ -27,37 +23,23 @@ 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 readonly MainWindowViewModel _viewModel;
private LiveViewPage? _liveViewPage;
private bool _isPhotoProcessRunning;
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 readonly DispatcherTimer _galleryPromptTimer = new() { Interval = TimeSpan.FromSeconds(5) };
private int _focusStatusDots;
@ -66,545 +48,207 @@ public partial class MainWindow : Window
AppSettingsService appSettings,
PictureGalleryService pictureGalleryService,
CameraService cameraService,
PhotoPrismUploadService photoPrismUploadService)
PhotoPrismUploadService photoPrismUploadService,
MainWindowViewModel viewModel)
{
this._logger = logger;
this._appSettings = appSettings;
this._pictureGalleryService = pictureGalleryService;
this._cameraService = cameraService;
this._photoPrismUploadService = photoPrismUploadService;
_logger = logger;
_appSettings = appSettings;
_pictureGalleryService = pictureGalleryService;
_cameraService = cameraService;
_photoPrismUploadService = photoPrismUploadService;
_viewModel = viewModel;
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 += (_, _) =>
// Wire ViewModel events to UI
_viewModel.LoadingProgressChanged += (status, count) =>
{
this._focusStatusDots = (this._focusStatusDots + 1) % 4;
this.CaptureStatusText.Text = $"Scharfstellen{new string('.', this._focusStatusDots)}";
LoadingStatusText.Text = status;
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
this._pictureGalleryService.NewPhotoCountChanged += PictureGalleryService_NewPhotoCountChanged;
// Wire service events
_pictureGalleryService.NewPhotoCountChanged += OnNewPhotoCountChanged;
TimerControlRectangleAnimation.OnTimerEllapsed += OnTimerElapsed;
this.DebugCloseButton.Visibility = Visibility.Collapsed;
this.HideDebugButton.Visibility = this._appSettings.IsDebugConsoleVisible ? Visibility.Visible : Visibility.Collapsed;
// Focus animation timer
_focusStatusAnimationTimer.Tick += (_, _) =>
{
_focusStatusDots = (_focusStatusDots + 1) % 4;
CaptureStatusText.Text = $"Scharfstellen{new string('.', _focusStatusDots)}";
};
// 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.");
}
});
// 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");
}
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
{
// 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");
_viewModel.OnTimerElapsed();
}
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
_logger.Error(ex.Message);
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;
StopFocusStatusAnimation();
CaptureStatusText.Visibility = Visibility.Collapsed;
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)
{
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;
_cameraService.CloseSession();
}
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);
}
_logger.Error($"Fehler beim Schließen der Kamera-Session: {ex.Message}");
}
_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)
{
this.HideGalleryPrompt();
this.ClosePicturePanel();
if (_viewModel.IsPhotoProcessRunning) return;
if (this._isPhotoProcessRunning)
{
return;
}
this._isPhotoProcessRunning = true;
ClosePicturePanel();
_viewModel.StartPhotoProcess();
try
{
SwitchButtonAndTimerPanel();
TimerControlRectangleAnimation.StartTimer(_appSettings.PhotoCountdownSeconds);
StartFocusStatusAnimation();
CaptureStatusText.Visibility = Visibility.Visible;
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 Task.Delay(TimeSpan.FromSeconds(_appSettings.FocusDelaySeconds));
if (!_viewModel.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();
await _cameraService.PrepareFocusAsync(_appSettings.FocusTimeoutMs);
StopFocusStatusAnimation();
CaptureStatusText.Visibility = Visibility.Collapsed;
}
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)
{
this._isShutdownSliderOpen = !this._isShutdownSliderOpen;
this.ShutdownToggleButton.Content = this._isShutdownSliderOpen ? ShutdownGlyphOpen : ShutdownGlyphClosed;
this.AnimateShutdownSlider(this._isShutdownSliderOpen);
}
private void AnimateShutdownSlider(bool open)
{
_isShutdownSliderOpen = !_isShutdownSliderOpen;
ShutdownToggleButton.Content = _isShutdownSliderOpen ? ShutdownGlyphOpen : ShutdownGlyphClosed;
var animation = new DoubleAnimation
{
To = open ? 0 : ShutdownSliderOffset,
To = _isShutdownSliderOpen ? 0 : ShutdownSliderOffset,
Duration = TimeSpan.FromMilliseconds(250),
EasingFunction = new QuadraticEase()
};
this.ShutdownSliderTransform.BeginAnimation(System.Windows.Media.TranslateTransform.XProperty, animation);
ShutdownSliderTransform.BeginAnimation(System.Windows.Media.TranslateTransform.XProperty, animation);
}
private void StartFocusStatusAnimation()
private async void ShutdownWindows(object sender, RoutedEventArgs e)
{
this._focusStatusDots = 0;
this.CaptureStatusText.Text = "Scharfstellen";
this._focusStatusAnimationTimer.Start();
}
private void StopFocusStatusAnimation()
var confirmDialog = new ContentDialog(DialogPresenter)
{
this._focusStatusAnimationTimer.Stop();
this._focusStatusDots = 0;
}
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)
};
private void ShowGalleryPrompt()
{
this.GalleryPrompt.Visibility = Visibility.Visible;
this._galleryPromptTimer.Stop();
this._galleryPromptTimer.Start();
}
if (await confirmDialog.ShowAsync() != ContentDialogResult.Primary) return;
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)
_cameraService.CloseSession();
}
catch
{
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;
}
var (args, errorMsg) = _appSettings.IsShutdownEnabled
? ("/s /t 0", "Windows konnte nicht heruntergefahren werden.")
: ("/l", "Abmeldung fehlgeschlagen.");
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)
try
{
// Show the photo dialog
await galleryPage.ShowPhotoDialogAsync(photoPath);
}
else
Process.Start(new ProcessStartInfo
{
this._logger.Error("PicturePanel content is not a PictureGalleryPage");
}
FileName = "shutdown",
Arguments = args,
CreateNoWindow = true,
UseShellExecute = false
});
}
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;
_logger.Error(ex.Message);
System.Windows.MessageBox.Show(errorMsg);
}
}
@ -612,28 +256,151 @@ public partial class MainWindow : Window
{
try
{
this._logger.Info("Zeige PhotoPrism Album-QR-Code an...");
// Generiere QR-Code
var qrCodeImage = this._photoPrismUploadService.GenerateAlbumQRCode();
var qrCodeImage = _photoPrismUploadService.GenerateAlbumQRCode();
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);
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}",
_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
}

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.");
}
}