Refactor: Simplify CameraService, improve session handling, and streamline application initialization
This commit is contained in:
parent
91935cd41c
commit
b3c91da331
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
148
src/CamBooth/CamBooth.App/MainWindowViewModel.cs
Normal file
148
src/CamBooth/CamBooth.App/MainWindowViewModel.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user