"Refactor TimerControl: Improve countdown animation, simplify focus logic, and add shutter flash effect"
This commit is contained in:
parent
b3c91da331
commit
a3c9f9b719
@ -1,42 +1,70 @@
|
||||
<UserControl x:Class="CamBooth.App.Features.LiveView.TimerControlRectangleAnimation"
|
||||
<UserControl x:Class="CamBooth.App.Features.LiveView.TimerControlRectangleAnimation"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:CamBooth.App.Features.LiveView"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="100" d:DesignWidth="1350">
|
||||
d:DesignHeight="900" d:DesignWidth="1600">
|
||||
<Grid>
|
||||
<!-- Hintergrund für den Timer -->
|
||||
<Border CornerRadius="10" Background="Black" Padding="0" Opacity="0" x:Name="TimerContainer">
|
||||
<Border x:Name="TimerContainer" Opacity="0">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<!-- Fortschrittsanzeige -->
|
||||
<Grid Height="75" Width="1350" Background="Gray" Margin="0,0,0,0">
|
||||
<Rectangle x:Name="ProgressBar"
|
||||
Fill="#4CAF50"
|
||||
Height="75"
|
||||
HorizontalAlignment="Left"/>
|
||||
<TextBlock x:Name="InstructionText"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
<!-- Circular countdown ring -->
|
||||
<Grid Width="360" Height="360">
|
||||
|
||||
<!-- Track ring -->
|
||||
<Ellipse Stroke="#1AFFFFFF"
|
||||
StrokeThickness="12"
|
||||
Width="320" Height="320"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="72"
|
||||
FontWeight="Bold"
|
||||
Foreground="White">Lächeln!</TextBlock>
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- Progress ring: StrokeDashArray[0] is animated from full to 0 -->
|
||||
<Ellipse x:Name="CountdownRing"
|
||||
Stroke="#D4AF37"
|
||||
StrokeThickness="12"
|
||||
Width="320" Height="320"
|
||||
StrokeDashArray="83.78 83.78"
|
||||
StrokeDashOffset="0"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Ellipse.RenderTransform>
|
||||
<!-- Rotate so the ring starts at 12 o'clock -->
|
||||
<RotateTransform Angle="-90"/>
|
||||
</Ellipse.RenderTransform>
|
||||
</Ellipse>
|
||||
|
||||
<!-- Countdown number -->
|
||||
<TextBlock x:Name="CountdownNumber"
|
||||
FontSize="180"
|
||||
FontWeight="Black"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Text="5">
|
||||
<TextBlock.RenderTransform>
|
||||
<ScaleTransform x:Name="CountdownNumberScale" ScaleX="1" ScaleY="1"/>
|
||||
</TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ~1~ Countdown-Anzeige @1@ -->
|
||||
<!-- <TextBlock x:Name="TimerText" -->
|
||||
<!-- FontSize="32" -->
|
||||
<!-- Foreground="White" -->
|
||||
<!-- HorizontalAlignment="Center" -->
|
||||
<!-- Text="00:00" -->
|
||||
<!-- Margin="0,10"/> -->
|
||||
<!-- ~1~ Status-Text @1@ -->
|
||||
<!-- <TextBlock x:Name="StatusText" -->
|
||||
<!-- FontSize="14" -->
|
||||
<!-- Foreground="Gray" -->
|
||||
<!-- HorizontalAlignment="Center" -->
|
||||
<!-- Text="Timer läuft..."/> -->
|
||||
<!-- Instruction text below the ring -->
|
||||
<TextBlock x:Name="InstructionText"
|
||||
FontSize="46"
|
||||
FontWeight="Bold"
|
||||
Foreground="#D4AF37"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
MaxWidth="900"
|
||||
Margin="0,24,0,0"
|
||||
Text="Lächeln! 😊"/>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
@ -1,144 +1,113 @@
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace CamBooth.App.Features.LiveView;
|
||||
|
||||
public partial class TimerControlRectangleAnimation : UserControl
|
||||
{
|
||||
public delegate void TimerElapsedEventHandler();
|
||||
// Ellipse diameter=320, StrokeThickness=12 → circumference = π*320/12 ≈ 83.78 dash-units
|
||||
private static readonly double RingFullDashUnits = Math.PI * 320.0 / 12.0;
|
||||
|
||||
public static event TimerElapsedEventHandler OnTimerEllapsed;
|
||||
public static event Action? OnTimerEllapsed;
|
||||
|
||||
private DispatcherTimer _timer;
|
||||
private readonly DispatcherTimer _ticker = new() { Interval = TimeSpan.FromSeconds(1) };
|
||||
private readonly Random _random = new();
|
||||
|
||||
private int _remainingTime; // Zeit in Sekunden
|
||||
private int _remainingTime;
|
||||
private double _totalDuration;
|
||||
|
||||
private double _totalDuration; // Gesamtzeit in Sekunden
|
||||
|
||||
private Storyboard _progressBarAnimation;
|
||||
|
||||
private Random _random = new Random();
|
||||
|
||||
private List<string> _photoInstructions = new List<string>
|
||||
{
|
||||
"Lächeln! 😊",
|
||||
"Hasenohren machen! 🐰",
|
||||
"Zunge rausstrecken! 👅",
|
||||
"Grimasse ziehen! 😝",
|
||||
"Daumen hoch! 👍",
|
||||
"Peace-Zeichen! ✌️",
|
||||
"Lustig gucken! 🤪",
|
||||
"Crazy Face! 🤯",
|
||||
"Küsschen! 😘",
|
||||
"Winken! 👋",
|
||||
"Herz mit den Händen! ❤️",
|
||||
"Verrückt sein! 🤪",
|
||||
"Überrascht schauen! 😲",
|
||||
"Cool bleiben! 😎",
|
||||
"Lachen! 😄",
|
||||
"Zähne zeigen! 😁",
|
||||
"Schnute ziehen! 😗",
|
||||
"Augen zukneifen! 😆",
|
||||
"Arm hochstrecken! 🙌",
|
||||
"Gruppe umarmen! 🤗"
|
||||
};
|
||||
private static readonly string[] Instructions =
|
||||
[
|
||||
"Lächeln! 😊", "Hasenohren machen! 🐰", "Zunge rausstrecken! 👅",
|
||||
"Grimasse ziehen! 😝", "Daumen hoch! 👍", "Peace-Zeichen! ✌️",
|
||||
"Lustig gucken! 🤪", "Crazy Face! 🤯", "Küsschen! 😘",
|
||||
"Winken! 👋", "Herz mit den Händen! ❤️", "Überrascht schauen! 😲",
|
||||
"Cool bleiben! 😎", "Lachen! 😄", "Zähne zeigen! 😁",
|
||||
"Schnute ziehen! 😗", "Arm hochstrecken! 🙌", "Gruppe umarmen! 🤗"
|
||||
];
|
||||
|
||||
|
||||
public TimerControlRectangleAnimation()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeTimer();
|
||||
}
|
||||
|
||||
private void InitializeTimer()
|
||||
{
|
||||
_timer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
_timer.Tick += OnTimerTick;
|
||||
}
|
||||
|
||||
|
||||
private void OnTimerTick(object sender, EventArgs e)
|
||||
{
|
||||
if (_remainingTime > 0)
|
||||
{
|
||||
_remainingTime--;
|
||||
|
||||
// TimerText.Text = TimeSpan.FromSeconds(_remainingTime).ToString(@"mm\:ss");
|
||||
}
|
||||
else
|
||||
{
|
||||
_timer.Stop();
|
||||
|
||||
// StatusText.Text = "Zeit abgelaufen!";
|
||||
StopProgressBarAnimation();
|
||||
|
||||
OnTimerEllapsed?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void StartTimer(int durationInSeconds)
|
||||
{
|
||||
_totalDuration = durationInSeconds;
|
||||
_remainingTime = durationInSeconds;
|
||||
|
||||
// TimerText.Text = TimeSpan.FromSeconds(_remainingTime).ToString(@"mm\:ss");
|
||||
// StatusText.Text = "Timer läuft...";
|
||||
|
||||
// Show initial random instruction
|
||||
ShowRandomInstruction();
|
||||
|
||||
_timer.Start();
|
||||
|
||||
StartProgressBarAnimation();
|
||||
ShowTimer();
|
||||
}
|
||||
|
||||
private void ShowTimer()
|
||||
{
|
||||
var fadeInAnimation = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(300));
|
||||
TimerContainer.BeginAnimation(OpacityProperty, fadeInAnimation);
|
||||
}
|
||||
|
||||
private void StartProgressBarAnimation()
|
||||
{
|
||||
// Fortschrittsbalken-Animation
|
||||
_progressBarAnimation = new Storyboard();
|
||||
var widthAnimation = new DoubleAnimation
|
||||
{
|
||||
From = 1350, // Volle Breite des Containers
|
||||
To = 0, // Endet bei 0 Breite
|
||||
Duration = TimeSpan.FromSeconds(_totalDuration),
|
||||
FillBehavior = FillBehavior.Stop
|
||||
};
|
||||
|
||||
Storyboard.SetTarget(widthAnimation, ProgressBar);
|
||||
Storyboard.SetTargetProperty(widthAnimation, new PropertyPath(Rectangle.WidthProperty));
|
||||
|
||||
_progressBarAnimation.Children.Add(widthAnimation);
|
||||
_progressBarAnimation.Begin();
|
||||
}
|
||||
|
||||
|
||||
private void StopProgressBarAnimation()
|
||||
{
|
||||
_progressBarAnimation?.Stop();
|
||||
}
|
||||
|
||||
private void ShowRandomInstruction()
|
||||
{
|
||||
if (_photoInstructions.Count > 0)
|
||||
{
|
||||
int randomIndex = _random.Next(_photoInstructions.Count);
|
||||
InstructionText.Text = _photoInstructions[randomIndex];
|
||||
}
|
||||
}
|
||||
public TimerControlRectangleAnimation()
|
||||
{
|
||||
InitializeComponent();
|
||||
_ticker.Tick += OnTick;
|
||||
}
|
||||
|
||||
|
||||
public void StartTimer(int durationInSeconds)
|
||||
{
|
||||
_totalDuration = durationInSeconds;
|
||||
_remainingTime = durationInSeconds;
|
||||
|
||||
CountdownNumber.Text = _remainingTime.ToString();
|
||||
InstructionText.Text = Instructions[_random.Next(Instructions.Length)];
|
||||
|
||||
// Reset ring offset so ring appears full
|
||||
CountdownRing.StrokeDashOffset = 0;
|
||||
CountdownRing.StrokeDashArray = new DoubleCollection { RingFullDashUnits, RingFullDashUnits };
|
||||
|
||||
FadeIn();
|
||||
StartRingAnimation();
|
||||
_ticker.Start();
|
||||
}
|
||||
|
||||
|
||||
private void OnTick(object? sender, EventArgs e)
|
||||
{
|
||||
_remainingTime--;
|
||||
|
||||
if (_remainingTime > 0)
|
||||
{
|
||||
CountdownNumber.Text = _remainingTime.ToString();
|
||||
AnimateNumberPop();
|
||||
}
|
||||
else
|
||||
{
|
||||
_ticker.Stop();
|
||||
CountdownNumber.Text = "0";
|
||||
AnimateNumberPop();
|
||||
StopRingAnimation();
|
||||
OnTimerEllapsed?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void FadeIn()
|
||||
{
|
||||
TimerContainer.BeginAnimation(OpacityProperty,
|
||||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(300)));
|
||||
}
|
||||
|
||||
|
||||
private void StartRingAnimation()
|
||||
{
|
||||
// StrokeDashOffset IS a DependencyProperty — animate directly, no Storyboard needed.
|
||||
// Pattern: dash=C, gap=C. Offset 0 → full ring; offset C → empty ring.
|
||||
CountdownRing.BeginAnimation(Shape.StrokeDashOffsetProperty,
|
||||
new DoubleAnimation(0, RingFullDashUnits,
|
||||
new Duration(TimeSpan.FromSeconds(_totalDuration)))
|
||||
{
|
||||
FillBehavior = FillBehavior.HoldEnd
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void StopRingAnimation() =>
|
||||
CountdownRing.BeginAnimation(Shape.StrokeDashOffsetProperty, null);
|
||||
|
||||
|
||||
private void AnimateNumberPop()
|
||||
{
|
||||
var easing = new BackEase { Amplitude = 0.4, EasingMode = EasingMode.EaseOut };
|
||||
CountdownNumberScale.ScaleX = 1.4;
|
||||
CountdownNumberScale.ScaleY = 1.4;
|
||||
CountdownNumberScale.BeginAnimation(ScaleTransform.ScaleXProperty,
|
||||
new DoubleAnimation(1.4, 1.0, TimeSpan.FromMilliseconds(450)) { EasingFunction = easing });
|
||||
CountdownNumberScale.BeginAnimation(ScaleTransform.ScaleYProperty,
|
||||
new DoubleAnimation(1.4, 1.0, TimeSpan.FromMilliseconds(450)) { EasingFunction = easing });
|
||||
}
|
||||
}
|
||||
@ -40,19 +40,16 @@
|
||||
Background="Transparent"
|
||||
Panel.ZIndex="1" />
|
||||
|
||||
<!-- Inhalt der dritten Zeile -->
|
||||
<StackPanel Grid.Row="0" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Bottom" Visibility="Hidden" Name="TimerPanel" Background="#AA000000" Panel.ZIndex="2"
|
||||
Margin="0 0 0 0">
|
||||
<TextBlock x:Name="CaptureStatusText"
|
||||
Text="Scharfstellen..."
|
||||
Visibility="Collapsed"
|
||||
Foreground="White"
|
||||
FontSize="36"
|
||||
FontWeight="Bold"
|
||||
<!-- Timer overlay: fullscreen so the ring centers on the camera view -->
|
||||
<Grid Grid.Row="0"
|
||||
x:Name="TimerPanel"
|
||||
Background="Transparent"
|
||||
Panel.ZIndex="2"
|
||||
Visibility="Hidden">
|
||||
<liveView:TimerControlRectangleAnimation x:Name="TimerControlRectangleAnimation"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="24 24 24 12"/>
|
||||
<liveView:TimerControlRectangleAnimation x:Name="TimerControlRectangleAnimation" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Action Buttons Container (bottom-right) -->
|
||||
<StackPanel Grid.Row="0"
|
||||
@ -387,6 +384,14 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Flash overlay: briefly flashes white on shutter release -->
|
||||
<Grid x:Name="FlashOverlay"
|
||||
Grid.RowSpan="2"
|
||||
Background="White"
|
||||
Opacity="0"
|
||||
Panel.ZIndex="9"
|
||||
IsHitTestVisible="False"/>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<Grid Grid.RowSpan="2"
|
||||
x:Name="LoadingOverlay"
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
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;
|
||||
@ -39,9 +37,6 @@ public partial class MainWindow : Window
|
||||
private const string ShutdownGlyphOpen = "\uE711";
|
||||
private const double ShutdownSliderOffset = 160;
|
||||
|
||||
private readonly DispatcherTimer _focusStatusAnimationTimer = new() { Interval = TimeSpan.FromMilliseconds(250) };
|
||||
private int _focusStatusDots;
|
||||
|
||||
|
||||
public MainWindow(
|
||||
Logger logger,
|
||||
@ -60,7 +55,7 @@ public partial class MainWindow : Window
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// Wire ViewModel events to UI
|
||||
// Wire ViewModel events to UI
|
||||
_viewModel.LoadingProgressChanged += (status, count) =>
|
||||
{
|
||||
LoadingStatusText.Text = status;
|
||||
@ -74,18 +69,11 @@ public partial class MainWindow : Window
|
||||
_viewModel.GalleryPromptRequested += () => GalleryPrompt.Visibility = Visibility.Visible;
|
||||
_viewModel.GalleryPromptDismissed += () => GalleryPrompt.Visibility = Visibility.Collapsed;
|
||||
|
||||
// Wire service events
|
||||
// Wire service events
|
||||
_pictureGalleryService.NewPhotoCountChanged += OnNewPhotoCountChanged;
|
||||
TimerControlRectangleAnimation.OnTimerEllapsed += OnTimerElapsed;
|
||||
|
||||
// Focus animation timer
|
||||
_focusStatusAnimationTimer.Tick += (_, _) =>
|
||||
{
|
||||
_focusStatusDots = (_focusStatusDots + 1) % 4;
|
||||
CaptureStatusText.Text = $"Scharfstellen{new string('.', _focusStatusDots)}";
|
||||
};
|
||||
|
||||
// Initial UI state
|
||||
// Initial UI state
|
||||
_isDebugConsoleVisible = _appSettings.IsDebugConsoleVisible;
|
||||
SetVisibilityDebugConsole(_isDebugConsoleVisible);
|
||||
SetVisibilityPicturePanel(false);
|
||||
@ -100,25 +88,22 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
|
||||
#region Event handlers (wired in XAML or in ctor)
|
||||
#region Event handlers
|
||||
|
||||
private void OnTimerElapsed()
|
||||
{
|
||||
SwitchButtonAndTimerPanel();
|
||||
try
|
||||
{
|
||||
_viewModel.OnTimerElapsed();
|
||||
// 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.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
StopFocusStatusAnimation();
|
||||
CaptureStatusText.Visibility = Visibility.Collapsed;
|
||||
SwitchButtonAndTimerPanel();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNewPhotoCountChanged(object? sender, int count)
|
||||
@ -142,7 +127,7 @@ public partial class MainWindow : Window
|
||||
_liveViewPage?.Dispose();
|
||||
}
|
||||
|
||||
// XAML-bound handlers
|
||||
// XAML-bound handlers
|
||||
private void StartExperience(object sender, RoutedEventArgs e)
|
||||
{
|
||||
StartLiveViewIfNeeded();
|
||||
@ -152,36 +137,21 @@ public partial class MainWindow : Window
|
||||
ShutdownDock.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private async void StartTakePhotoProcess(object sender, RoutedEventArgs e)
|
||||
private void StartTakePhotoProcess(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel.IsPhotoProcessRunning) return;
|
||||
|
||||
ClosePicturePanel();
|
||||
_viewModel.StartPhotoProcess();
|
||||
|
||||
try
|
||||
{
|
||||
SwitchButtonAndTimerPanel();
|
||||
TimerControlRectangleAnimation.StartTimer(_appSettings.PhotoCountdownSeconds);
|
||||
StartFocusStatusAnimation();
|
||||
CaptureStatusText.Visibility = Visibility.Visible;
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(_appSettings.FocusDelaySeconds));
|
||||
if (!_viewModel.IsPhotoProcessRunning) return;
|
||||
|
||||
await _cameraService.PrepareFocusAsync(_appSettings.FocusTimeoutMs);
|
||||
StopFocusStatusAnimation();
|
||||
CaptureStatusText.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_viewModel.CancelPhotoProcess();
|
||||
StopFocusStatusAnimation();
|
||||
CaptureStatusText.Visibility = Visibility.Collapsed;
|
||||
if (TimerPanel.Visibility == Visibility.Visible)
|
||||
SwitchButtonAndTimerPanel();
|
||||
_logger.Error(ex.Message);
|
||||
}
|
||||
// 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) =>
|
||||
@ -203,7 +173,7 @@ public partial class MainWindow : Window
|
||||
Duration = TimeSpan.FromMilliseconds(250),
|
||||
EasingFunction = new QuadraticEase()
|
||||
};
|
||||
ShutdownSliderTransform.BeginAnimation(System.Windows.Media.TranslateTransform.XProperty, animation);
|
||||
ShutdownSliderTransform.BeginAnimation(TranslateTransform.XProperty, animation);
|
||||
}
|
||||
|
||||
private async void ShutdownWindows(object sender, RoutedEventArgs e)
|
||||
@ -360,17 +330,14 @@ public partial class MainWindow : Window
|
||||
TimerPanel.Visibility = TimerPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
|
||||
private void StartFocusStatusAnimation()
|
||||
/// <summary>Briefly flashes the screen white to simulate a camera shutter.</summary>
|
||||
private void TriggerShutterFlash()
|
||||
{
|
||||
_focusStatusDots = 0;
|
||||
CaptureStatusText.Text = "Scharfstellen";
|
||||
_focusStatusAnimationTimer.Start();
|
||||
}
|
||||
|
||||
private void StopFocusStatusAnimation()
|
||||
FlashOverlay.BeginAnimation(OpacityProperty,
|
||||
new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(450))
|
||||
{
|
||||
_focusStatusAnimationTimer.Stop();
|
||||
_focusStatusDots = 0;
|
||||
EasingFunction = new PowerEase { Power = 2, EasingMode = EasingMode.EaseIn }
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ShowLatestPhotoDialogAsync()
|
||||
|
||||
@ -21,6 +21,9 @@ public class MainWindowViewModel
|
||||
|
||||
public bool IsPhotoProcessRunning { get; private set; }
|
||||
|
||||
// The running focus task — shared between StartTakePhotoProcess and OnTimerElapsed
|
||||
private Task _focusTask = Task.CompletedTask;
|
||||
|
||||
// Events → MainWindow subscribes and updates UI
|
||||
public event Action<string, string>? LoadingProgressChanged; // (statusText, countText)
|
||||
public event Action? InitializationCompleted;
|
||||
@ -68,10 +71,30 @@ public class MainWindowViewModel
|
||||
|
||||
|
||||
/// <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.
|
||||
/// Starts autofocus in the background. The effective timeout is capped so focus
|
||||
/// is guaranteed to complete before the countdown timer fires.
|
||||
/// </summary>
|
||||
public void OnTimerElapsed()
|
||||
public Task BeginFocusAsync(int delaySeconds, int focusTimeoutMs, int countdownSeconds)
|
||||
{
|
||||
// Ensure focus always finishes at least 500ms before the timer fires.
|
||||
var maxAllowedMs = (countdownSeconds - delaySeconds) * 1000 - 500;
|
||||
var effectiveTimeoutMs = Math.Max(200, Math.Min(focusTimeoutMs, maxAllowedMs));
|
||||
_focusTask = RunFocusAsync(delaySeconds, effectiveTimeoutMs);
|
||||
return _focusTask;
|
||||
}
|
||||
|
||||
private async Task RunFocusAsync(int delaySeconds, int focusTimeoutMs)
|
||||
{
|
||||
if (delaySeconds > 0)
|
||||
await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
|
||||
await _cameraService.PrepareFocusAsync(focusTimeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the timer fires. Focus is already guaranteed complete — takes photo immediately.
|
||||
/// Throws on camera error so the caller can show a message.
|
||||
/// </summary>
|
||||
public void TakePhotoAfterTimer()
|
||||
{
|
||||
IsPhotoProcessRunning = false;
|
||||
_cameraService.TakePhoto();
|
||||
@ -82,6 +105,7 @@ public class MainWindowViewModel
|
||||
public void StartPhotoProcess()
|
||||
{
|
||||
IsPhotoProcessRunning = true;
|
||||
_focusTask = Task.CompletedTask;
|
||||
DismissGalleryPrompt();
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user