377 lines
13 KiB
C#
377 lines
13 KiB
C#
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using CamBooth.App.Core.AppSettings;
|
|
using CamBooth.App.Core.Logging;
|
|
using CamBooth.App.Features.Camera;
|
|
using CamBooth.App.Features.DebugConsole;
|
|
using CamBooth.App.Features.LiveView;
|
|
using CamBooth.App.Features.PhotoPrismUpload;
|
|
using CamBooth.App.Features.PictureGallery;
|
|
|
|
namespace CamBooth.App;
|
|
|
|
/// <summary>
|
|
/// Interaction logic for MainWindow.xaml
|
|
/// </summary>
|
|
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 readonly MainWindowViewModel _viewModel;
|
|
|
|
private LiveViewPage? _liveViewPage;
|
|
private bool _isCameraStarted;
|
|
private bool _isPicturePanelVisible;
|
|
private bool _isDebugConsoleVisible;
|
|
private bool _isShutdownSliderOpen;
|
|
|
|
private const string ShutdownGlyphClosed = "\uE7E8";
|
|
private const string ShutdownGlyphOpen = "\uE711";
|
|
private const double ShutdownSliderOffset = 160;
|
|
|
|
|
|
public MainWindow(
|
|
Logger logger,
|
|
AppSettingsService appSettings,
|
|
PictureGalleryService pictureGalleryService,
|
|
CameraService cameraService,
|
|
PhotoPrismUploadService photoPrismUploadService,
|
|
MainWindowViewModel viewModel)
|
|
{
|
|
_logger = logger;
|
|
_appSettings = appSettings;
|
|
_pictureGalleryService = pictureGalleryService;
|
|
_cameraService = cameraService;
|
|
_photoPrismUploadService = photoPrismUploadService;
|
|
_viewModel = viewModel;
|
|
|
|
InitializeComponent();
|
|
|
|
// Wire ViewModel events to UI
|
|
_viewModel.LoadingProgressChanged += (status, count) =>
|
|
{
|
|
LoadingStatusText.Text = status;
|
|
LoadingCountText.Text = count;
|
|
};
|
|
_viewModel.InitializationCompleted += () =>
|
|
{
|
|
LoadingOverlay.Visibility = Visibility.Collapsed;
|
|
WelcomeOverlay.Visibility = Visibility.Visible;
|
|
};
|
|
_viewModel.GalleryPromptRequested += () => GalleryPrompt.Visibility = Visibility.Visible;
|
|
_viewModel.GalleryPromptDismissed += () => GalleryPrompt.Visibility = Visibility.Collapsed;
|
|
|
|
// Wire service events
|
|
_pictureGalleryService.NewPhotoCountChanged += OnNewPhotoCountChanged;
|
|
TimerControlRectangleAnimation.OnTimerEllapsed += OnTimerElapsed;
|
|
|
|
// Initial UI state
|
|
_isDebugConsoleVisible = _appSettings.IsDebugConsoleVisible;
|
|
SetVisibilityDebugConsole(_isDebugConsoleVisible);
|
|
SetVisibilityPicturePanel(false);
|
|
DebugCloseButton.Visibility = Visibility.Collapsed;
|
|
HideDebugButton.Visibility = _appSettings.IsDebugConsoleVisible ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
Closing += OnClosing;
|
|
|
|
_ = _viewModel.InitializeAsync();
|
|
_logger.Info($"config file loaded: '{appSettings.ConfigFileName}'");
|
|
_logger.Info("MainWindow initialized");
|
|
}
|
|
|
|
|
|
#region Event handlers
|
|
|
|
private void OnTimerElapsed()
|
|
{
|
|
SwitchButtonAndTimerPanel();
|
|
try
|
|
{
|
|
// Focus is guaranteed complete before the timer fires (bounded timeout).
|
|
_viewModel.TakePhotoAfterTimer();
|
|
TriggerShutterFlash();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex.Message);
|
|
System.Windows.MessageBox.Show("Sorry, da ging was schief! Bitte nochmal probieren.");
|
|
}
|
|
}
|
|
|
|
private void OnNewPhotoCountChanged(object? sender, int count)
|
|
{
|
|
NewPhotosBadge.Visibility = count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
|
if (count > 0)
|
|
NewPhotoCountText.Text = count.ToString();
|
|
}
|
|
|
|
private void OnClosing(object? sender, CancelEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
_cameraService.CloseSession();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"Fehler beim Schließen der Kamera-Session: {ex.Message}");
|
|
}
|
|
|
|
_liveViewPage?.Dispose();
|
|
}
|
|
|
|
// XAML-bound handlers
|
|
private void StartExperience(object sender, RoutedEventArgs e)
|
|
{
|
|
StartLiveViewIfNeeded();
|
|
WelcomeOverlay.Visibility = Visibility.Collapsed;
|
|
ButtonPanel.Visibility = Visibility.Visible;
|
|
ActionButtonsContainer.Visibility = Visibility.Visible;
|
|
//ShutdownDock.Visibility = Visibility.Visible;
|
|
}
|
|
|
|
private void StartTakePhotoProcess(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_viewModel.IsPhotoProcessRunning) return;
|
|
|
|
ClosePicturePanel();
|
|
_viewModel.StartPhotoProcess();
|
|
SwitchButtonAndTimerPanel();
|
|
TimerControlRectangleAnimation.StartTimer(_appSettings.PhotoCountdownSeconds);
|
|
|
|
// Autofocus runs silently in the background during the countdown.
|
|
// The effective timeout is capped so focus always completes before the timer fires.
|
|
_ = _viewModel.BeginFocusAsync(
|
|
_appSettings.FocusDelaySeconds,
|
|
_appSettings.FocusTimeoutMs,
|
|
_appSettings.PhotoCountdownSeconds);
|
|
}
|
|
|
|
private void SetVisibilityPicturePanel(object sender, MouseButtonEventArgs e) =>
|
|
SetVisibilityPicturePanel(!_isPicturePanelVisible);
|
|
|
|
private void SetVisibilityDebugConsole(object sender, RoutedEventArgs e)
|
|
{
|
|
_isDebugConsoleVisible = !_isDebugConsoleVisible;
|
|
SetVisibilityDebugConsole(_isDebugConsoleVisible);
|
|
}
|
|
|
|
// private void ToggleShutdownSlider(object sender, RoutedEventArgs e)
|
|
// {
|
|
// _isShutdownSliderOpen = !_isShutdownSliderOpen;
|
|
// ShutdownToggleButton.Content = _isShutdownSliderOpen ? ShutdownGlyphOpen : ShutdownGlyphClosed;
|
|
// var animation = new DoubleAnimation
|
|
// {
|
|
// To = _isShutdownSliderOpen ? 0 : ShutdownSliderOffset,
|
|
// Duration = TimeSpan.FromMilliseconds(250),
|
|
// EasingFunction = new QuadraticEase()
|
|
// };
|
|
// ShutdownSliderTransform.BeginAnimation(TranslateTransform.XProperty, animation);
|
|
// }
|
|
|
|
// private async void ShutdownWindows(object sender, RoutedEventArgs e)
|
|
// {
|
|
// bool confirmed = await ShutdownConfirmOverlay.ShowAsync();
|
|
// if (!confirmed) return;
|
|
//
|
|
// try
|
|
// {
|
|
// _cameraService.CloseSession();
|
|
// }
|
|
// catch
|
|
// {
|
|
// }
|
|
//
|
|
// var (args, errorMsg) = _appSettings.IsShutdownEnabled
|
|
// ? ("/s /t 0", "Windows konnte nicht heruntergefahren werden.")
|
|
// : ("/l", "Abmeldung fehlgeschlagen.");
|
|
//
|
|
// try
|
|
// {
|
|
// Process.Start(new ProcessStartInfo
|
|
// {
|
|
// FileName = "shutdown",
|
|
// Arguments = args,
|
|
// CreateNoWindow = true,
|
|
// UseShellExecute = false
|
|
// });
|
|
// }
|
|
// catch (Exception ex)
|
|
// {
|
|
// _logger.Error(ex.Message);
|
|
// System.Windows.MessageBox.Show(errorMsg);
|
|
// }
|
|
// }
|
|
|
|
private void ShowQRCode(object sender, MouseButtonEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var qrCodeImage = _photoPrismUploadService.GenerateAlbumQRCode();
|
|
if (qrCodeImage == null)
|
|
{
|
|
System.Windows.MessageBox.Show(
|
|
"QR-Code konnte nicht generiert werden. Bitte überprüfe die PhotoPrism-Konfiguration.",
|
|
"Fehler", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
var content = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
|
|
content.Children.Add(new System.Windows.Controls.Image
|
|
{
|
|
Source = qrCodeImage,
|
|
Width = 400,
|
|
Height = 400,
|
|
Margin = new Thickness(0, 0, 0, 20)
|
|
});
|
|
content.Children.Add(new System.Windows.Controls.TextBlock
|
|
{
|
|
Text = "Mit einem QR-Code-Scanner scannen",
|
|
Foreground = Brushes.White,
|
|
FontSize = 16,
|
|
TextAlignment = TextAlignment.Center
|
|
});
|
|
|
|
QRCodeOverlay.DialogContent = content;
|
|
QRCodeOverlay.Show();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"Fehler beim Anzeigen des QR-Codes: {ex.Message}");
|
|
System.Windows.MessageBox.Show(
|
|
$"Fehler beim Anzeigen des QR-Codes: {ex.Message}",
|
|
"Fehler", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private async void OpenGalleryFromPrompt(object sender, RoutedEventArgs e)
|
|
{
|
|
_viewModel.DismissGalleryPrompt();
|
|
SetVisibilityPicturePanel(true);
|
|
await Task.Delay(300);
|
|
await ShowLatestPhotoDialogAsync();
|
|
}
|
|
|
|
private void CloseApp(object sender, RoutedEventArgs e) => Close();
|
|
|
|
#endregion
|
|
|
|
|
|
#region UI helpers
|
|
|
|
private void StartLiveViewIfNeeded()
|
|
{
|
|
if (_isCameraStarted) return;
|
|
try
|
|
{
|
|
_liveViewPage = new LiveViewPage(_logger, _cameraService);
|
|
MainFrame.Navigate(_liveViewPage);
|
|
_isCameraStarted = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"Failed to start live view: {ex.Message}\n{ex.StackTrace}");
|
|
System.Windows.MessageBox.Show(
|
|
$"Failed to initialize camera. Please ensure the camera is properly connected.\n\nError: {ex.Message}",
|
|
"Camera Connection Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
public void ClosePicturePanel()
|
|
{
|
|
if (PicturePanel.Content is PictureGalleryPage page)
|
|
page.CloseOpenDialog();
|
|
if (PicturePanel.Content is null) return;
|
|
SetVisibilityPicturePanel(false);
|
|
}
|
|
|
|
private void SetVisibilityPicturePanel(bool visible)
|
|
{
|
|
if (visible)
|
|
{
|
|
_viewModel.DismissGalleryPrompt();
|
|
PicturePanel.Navigate(new PictureGalleryPage(_appSettings, _logger, _pictureGalleryService,
|
|
_photoPrismUploadService));
|
|
_pictureGalleryService.ResetNewPhotoCount();
|
|
ButtonPanel.Visibility = Visibility.Hidden;
|
|
ActionButtonsContainer.Visibility = Visibility.Hidden;
|
|
//ShutdownDock.Visibility = Visibility.Hidden;
|
|
}
|
|
else
|
|
{
|
|
PicturePanel.ClearValue(ContentProperty);
|
|
ButtonPanel.Visibility = Visibility.Visible;
|
|
ActionButtonsContainer.Visibility = Visibility.Visible;
|
|
//ShutdownDock.Visibility = Visibility.Visible;
|
|
}
|
|
|
|
_isPicturePanelVisible = visible;
|
|
}
|
|
|
|
private void SetVisibilityDebugConsole(bool visible)
|
|
{
|
|
if (!_appSettings.IsDebugConsoleVisible) return;
|
|
if (visible)
|
|
DebugFrame.Navigate(new DebugConsolePage(_logger));
|
|
else
|
|
DebugFrame.ClearValue(ContentProperty);
|
|
}
|
|
|
|
private void SwitchButtonAndTimerPanel()
|
|
{
|
|
ButtonPanel.Visibility = ButtonPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
|
|
ActionButtonsContainer.Visibility = ActionButtonsContainer.Visibility == Visibility.Hidden
|
|
? Visibility.Visible
|
|
: Visibility.Hidden;
|
|
TimerPanel.Visibility = TimerPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
|
|
}
|
|
|
|
/// <summary>Briefly flashes the screen white to simulate a camera shutter.</summary>
|
|
private void TriggerShutterFlash()
|
|
{
|
|
FlashOverlay.BeginAnimation(OpacityProperty,
|
|
new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(450))
|
|
{
|
|
EasingFunction = new PowerEase { Power = 2, EasingMode = EasingMode.EaseIn }
|
|
});
|
|
}
|
|
|
|
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
|
|
}
|