"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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:local="clr-namespace:CamBooth.App.Features.LiveView"
|
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="100" d:DesignWidth="1350">
|
d:DesignHeight="900" d:DesignWidth="1600">
|
||||||
<Grid>
|
<Grid>
|
||||||
<!-- Hintergrund für den Timer -->
|
<Border x:Name="TimerContainer" Opacity="0">
|
||||||
<Border CornerRadius="10" Background="Black" Padding="0" Opacity="0" x:Name="TimerContainer">
|
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
<!-- Fortschrittsanzeige -->
|
|
||||||
<Grid Height="75" Width="1350" Background="Gray" Margin="0,0,0,0">
|
<!-- Circular countdown ring -->
|
||||||
<Rectangle x:Name="ProgressBar"
|
<Grid Width="360" Height="360">
|
||||||
Fill="#4CAF50"
|
|
||||||
Height="75"
|
<!-- Track ring -->
|
||||||
HorizontalAlignment="Left"/>
|
<Ellipse Stroke="#1AFFFFFF"
|
||||||
<TextBlock x:Name="InstructionText"
|
StrokeThickness="12"
|
||||||
VerticalAlignment="Center"
|
Width="320" Height="320"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
FontSize="72"
|
VerticalAlignment="Center"/>
|
||||||
FontWeight="Bold"
|
|
||||||
Foreground="White">Lächeln!</TextBlock>
|
<!-- 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>
|
</Grid>
|
||||||
|
|
||||||
<!-- ~1~ Countdown-Anzeige @1@ -->
|
<!-- Instruction text below the ring -->
|
||||||
<!-- <TextBlock x:Name="TimerText" -->
|
<TextBlock x:Name="InstructionText"
|
||||||
<!-- FontSize="32" -->
|
FontSize="46"
|
||||||
<!-- Foreground="White" -->
|
FontWeight="Bold"
|
||||||
<!-- HorizontalAlignment="Center" -->
|
Foreground="#D4AF37"
|
||||||
<!-- Text="00:00" -->
|
HorizontalAlignment="Center"
|
||||||
<!-- Margin="0,10"/> -->
|
TextAlignment="Center"
|
||||||
<!-- ~1~ Status-Text @1@ -->
|
TextWrapping="Wrap"
|
||||||
<!-- <TextBlock x:Name="StatusText" -->
|
MaxWidth="900"
|
||||||
<!-- FontSize="14" -->
|
Margin="0,24,0,0"
|
||||||
<!-- Foreground="Gray" -->
|
Text="Lächeln! 😊"/>
|
||||||
<!-- HorizontalAlignment="Center" -->
|
|
||||||
<!-- Text="Timer läuft..."/> -->
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -1,144 +1,113 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace CamBooth.App.Features.LiveView;
|
namespace CamBooth.App.Features.LiveView;
|
||||||
|
|
||||||
public partial class TimerControlRectangleAnimation : UserControl
|
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 static readonly string[] Instructions =
|
||||||
|
[
|
||||||
private Storyboard _progressBarAnimation;
|
"Lächeln! 😊", "Hasenohren machen! 🐰", "Zunge rausstrecken! 👅",
|
||||||
|
"Grimasse ziehen! 😝", "Daumen hoch! 👍", "Peace-Zeichen! ✌️",
|
||||||
private Random _random = new Random();
|
"Lustig gucken! 🤪", "Crazy Face! 🤯", "Küsschen! 😘",
|
||||||
|
"Winken! 👋", "Herz mit den Händen! ❤️", "Überrascht schauen! 😲",
|
||||||
private List<string> _photoInstructions = new List<string>
|
"Cool bleiben! 😎", "Lachen! 😄", "Zähne zeigen! 😁",
|
||||||
{
|
"Schnute ziehen! 😗", "Arm hochstrecken! 🙌", "Gruppe umarmen! 🤗"
|
||||||
"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! 🤗"
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
public TimerControlRectangleAnimation()
|
public TimerControlRectangleAnimation()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
InitializeTimer();
|
_ticker.Tick += OnTick;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeTimer()
|
|
||||||
{
|
|
||||||
_timer = new DispatcherTimer
|
|
||||||
{
|
|
||||||
Interval = TimeSpan.FromSeconds(1)
|
|
||||||
};
|
|
||||||
_timer.Tick += OnTimerTick;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void OnTimerTick(object sender, EventArgs e)
|
public void StartTimer(int durationInSeconds)
|
||||||
{
|
{
|
||||||
if (_remainingTime > 0)
|
_totalDuration = durationInSeconds;
|
||||||
{
|
_remainingTime = durationInSeconds;
|
||||||
_remainingTime--;
|
|
||||||
|
|
||||||
// TimerText.Text = TimeSpan.FromSeconds(_remainingTime).ToString(@"mm\:ss");
|
CountdownNumber.Text = _remainingTime.ToString();
|
||||||
}
|
InstructionText.Text = Instructions[_random.Next(Instructions.Length)];
|
||||||
else
|
|
||||||
{
|
|
||||||
_timer.Stop();
|
|
||||||
|
|
||||||
// StatusText.Text = "Zeit abgelaufen!";
|
// Reset ring offset so ring appears full
|
||||||
StopProgressBarAnimation();
|
CountdownRing.StrokeDashOffset = 0;
|
||||||
|
CountdownRing.StrokeDashArray = new DoubleCollection { RingFullDashUnits, RingFullDashUnits };
|
||||||
OnTimerEllapsed?.Invoke();
|
|
||||||
}
|
FadeIn();
|
||||||
}
|
StartRingAnimation();
|
||||||
|
_ticker.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void StartTimer(int durationInSeconds)
|
private void OnTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_totalDuration = durationInSeconds;
|
_remainingTime--;
|
||||||
_remainingTime = durationInSeconds;
|
|
||||||
|
|
||||||
// TimerText.Text = TimeSpan.FromSeconds(_remainingTime).ToString(@"mm\:ss");
|
if (_remainingTime > 0)
|
||||||
// StatusText.Text = "Timer läuft...";
|
{
|
||||||
|
CountdownNumber.Text = _remainingTime.ToString();
|
||||||
// Show initial random instruction
|
AnimateNumberPop();
|
||||||
ShowRandomInstruction();
|
}
|
||||||
|
else
|
||||||
_timer.Start();
|
{
|
||||||
|
_ticker.Stop();
|
||||||
StartProgressBarAnimation();
|
CountdownNumber.Text = "0";
|
||||||
ShowTimer();
|
AnimateNumberPop();
|
||||||
}
|
StopRingAnimation();
|
||||||
|
OnTimerEllapsed?.Invoke();
|
||||||
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()
|
private void FadeIn()
|
||||||
{
|
{
|
||||||
_progressBarAnimation?.Stop();
|
TimerContainer.BeginAnimation(OpacityProperty,
|
||||||
}
|
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(300)));
|
||||||
|
}
|
||||||
|
|
||||||
private void ShowRandomInstruction()
|
|
||||||
{
|
private void StartRingAnimation()
|
||||||
if (_photoInstructions.Count > 0)
|
{
|
||||||
{
|
// StrokeDashOffset IS a DependencyProperty — animate directly, no Storyboard needed.
|
||||||
int randomIndex = _random.Next(_photoInstructions.Count);
|
// Pattern: dash=C, gap=C. Offset 0 → full ring; offset C → empty ring.
|
||||||
InstructionText.Text = _photoInstructions[randomIndex];
|
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"
|
Background="Transparent"
|
||||||
Panel.ZIndex="1" />
|
Panel.ZIndex="1" />
|
||||||
|
|
||||||
<!-- Inhalt der dritten Zeile -->
|
<!-- Timer overlay: fullscreen so the ring centers on the camera view -->
|
||||||
<StackPanel Grid.Row="0" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Bottom" Visibility="Hidden" Name="TimerPanel" Background="#AA000000" Panel.ZIndex="2"
|
<Grid Grid.Row="0"
|
||||||
Margin="0 0 0 0">
|
x:Name="TimerPanel"
|
||||||
<TextBlock x:Name="CaptureStatusText"
|
Background="Transparent"
|
||||||
Text="Scharfstellen..."
|
Panel.ZIndex="2"
|
||||||
Visibility="Collapsed"
|
Visibility="Hidden">
|
||||||
Foreground="White"
|
<liveView:TimerControlRectangleAnimation x:Name="TimerControlRectangleAnimation"
|
||||||
FontSize="36"
|
HorizontalAlignment="Center"
|
||||||
FontWeight="Bold"
|
VerticalAlignment="Center"/>
|
||||||
HorizontalAlignment="Center"
|
</Grid>
|
||||||
Margin="24 24 24 12"/>
|
|
||||||
<liveView:TimerControlRectangleAnimation x:Name="TimerControlRectangleAnimation" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Action Buttons Container (bottom-right) -->
|
<!-- Action Buttons Container (bottom-right) -->
|
||||||
<StackPanel Grid.Row="0"
|
<StackPanel Grid.Row="0"
|
||||||
@ -387,6 +384,14 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</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 -->
|
<!-- Loading Overlay -->
|
||||||
<Grid Grid.RowSpan="2"
|
<Grid Grid.RowSpan="2"
|
||||||
x:Name="LoadingOverlay"
|
x:Name="LoadingOverlay"
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Threading;
|
|
||||||
using CamBooth.App.Core.AppSettings;
|
using CamBooth.App.Core.AppSettings;
|
||||||
using CamBooth.App.Core.Logging;
|
using CamBooth.App.Core.Logging;
|
||||||
using CamBooth.App.Features.Camera;
|
using CamBooth.App.Features.Camera;
|
||||||
@ -39,9 +37,6 @@ public partial class MainWindow : Window
|
|||||||
private const string ShutdownGlyphOpen = "\uE711";
|
private const string ShutdownGlyphOpen = "\uE711";
|
||||||
private const double ShutdownSliderOffset = 160;
|
private const double ShutdownSliderOffset = 160;
|
||||||
|
|
||||||
private readonly DispatcherTimer _focusStatusAnimationTimer = new() { Interval = TimeSpan.FromMilliseconds(250) };
|
|
||||||
private int _focusStatusDots;
|
|
||||||
|
|
||||||
|
|
||||||
public MainWindow(
|
public MainWindow(
|
||||||
Logger logger,
|
Logger logger,
|
||||||
@ -60,7 +55,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
// Wire ViewModel events to UI
|
// Wire ViewModel events to UI
|
||||||
_viewModel.LoadingProgressChanged += (status, count) =>
|
_viewModel.LoadingProgressChanged += (status, count) =>
|
||||||
{
|
{
|
||||||
LoadingStatusText.Text = status;
|
LoadingStatusText.Text = status;
|
||||||
@ -74,18 +69,11 @@ public partial class MainWindow : Window
|
|||||||
_viewModel.GalleryPromptRequested += () => GalleryPrompt.Visibility = Visibility.Visible;
|
_viewModel.GalleryPromptRequested += () => GalleryPrompt.Visibility = Visibility.Visible;
|
||||||
_viewModel.GalleryPromptDismissed += () => GalleryPrompt.Visibility = Visibility.Collapsed;
|
_viewModel.GalleryPromptDismissed += () => GalleryPrompt.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
// Wire service events
|
// Wire service events
|
||||||
_pictureGalleryService.NewPhotoCountChanged += OnNewPhotoCountChanged;
|
_pictureGalleryService.NewPhotoCountChanged += OnNewPhotoCountChanged;
|
||||||
TimerControlRectangleAnimation.OnTimerEllapsed += OnTimerElapsed;
|
TimerControlRectangleAnimation.OnTimerEllapsed += OnTimerElapsed;
|
||||||
|
|
||||||
// Focus animation timer
|
// Initial UI state
|
||||||
_focusStatusAnimationTimer.Tick += (_, _) =>
|
|
||||||
{
|
|
||||||
_focusStatusDots = (_focusStatusDots + 1) % 4;
|
|
||||||
CaptureStatusText.Text = $"Scharfstellen{new string('.', _focusStatusDots)}";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial UI state
|
|
||||||
_isDebugConsoleVisible = _appSettings.IsDebugConsoleVisible;
|
_isDebugConsoleVisible = _appSettings.IsDebugConsoleVisible;
|
||||||
SetVisibilityDebugConsole(_isDebugConsoleVisible);
|
SetVisibilityDebugConsole(_isDebugConsoleVisible);
|
||||||
SetVisibilityPicturePanel(false);
|
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()
|
private void OnTimerElapsed()
|
||||||
{
|
{
|
||||||
|
SwitchButtonAndTimerPanel();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_viewModel.OnTimerElapsed();
|
// Focus is guaranteed complete before the timer fires (bounded timeout).
|
||||||
|
_viewModel.TakePhotoAfterTimer();
|
||||||
|
TriggerShutterFlash();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex.Message);
|
_logger.Error(ex.Message);
|
||||||
System.Windows.MessageBox.Show("Sorry, da ging was schief! Bitte nochmal probieren.");
|
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)
|
private void OnNewPhotoCountChanged(object? sender, int count)
|
||||||
@ -142,7 +127,7 @@ public partial class MainWindow : Window
|
|||||||
_liveViewPage?.Dispose();
|
_liveViewPage?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// XAML-bound handlers
|
// XAML-bound handlers
|
||||||
private void StartExperience(object sender, RoutedEventArgs e)
|
private void StartExperience(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
StartLiveViewIfNeeded();
|
StartLiveViewIfNeeded();
|
||||||
@ -152,36 +137,21 @@ public partial class MainWindow : Window
|
|||||||
ShutdownDock.Visibility = Visibility.Visible;
|
ShutdownDock.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void StartTakePhotoProcess(object sender, RoutedEventArgs e)
|
private void StartTakePhotoProcess(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_viewModel.IsPhotoProcessRunning) return;
|
if (_viewModel.IsPhotoProcessRunning) return;
|
||||||
|
|
||||||
ClosePicturePanel();
|
ClosePicturePanel();
|
||||||
_viewModel.StartPhotoProcess();
|
_viewModel.StartPhotoProcess();
|
||||||
|
SwitchButtonAndTimerPanel();
|
||||||
|
TimerControlRectangleAnimation.StartTimer(_appSettings.PhotoCountdownSeconds);
|
||||||
|
|
||||||
try
|
// Autofocus runs silently in the background during the countdown.
|
||||||
{
|
// The effective timeout is capped so focus always completes before the timer fires.
|
||||||
SwitchButtonAndTimerPanel();
|
_ = _viewModel.BeginFocusAsync(
|
||||||
TimerControlRectangleAnimation.StartTimer(_appSettings.PhotoCountdownSeconds);
|
_appSettings.FocusDelaySeconds,
|
||||||
StartFocusStatusAnimation();
|
_appSettings.FocusTimeoutMs,
|
||||||
CaptureStatusText.Visibility = Visibility.Visible;
|
_appSettings.PhotoCountdownSeconds);
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(_appSettings.FocusDelaySeconds));
|
|
||||||
if (!_viewModel.IsPhotoProcessRunning) return;
|
|
||||||
|
|
||||||
await _cameraService.PrepareFocusAsync(_appSettings.FocusTimeoutMs);
|
|
||||||
StopFocusStatusAnimation();
|
|
||||||
CaptureStatusText.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_viewModel.CancelPhotoProcess();
|
|
||||||
StopFocusStatusAnimation();
|
|
||||||
CaptureStatusText.Visibility = Visibility.Collapsed;
|
|
||||||
if (TimerPanel.Visibility == Visibility.Visible)
|
|
||||||
SwitchButtonAndTimerPanel();
|
|
||||||
_logger.Error(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetVisibilityPicturePanel(object sender, MouseButtonEventArgs e) =>
|
private void SetVisibilityPicturePanel(object sender, MouseButtonEventArgs e) =>
|
||||||
@ -203,7 +173,7 @@ public partial class MainWindow : Window
|
|||||||
Duration = TimeSpan.FromMilliseconds(250),
|
Duration = TimeSpan.FromMilliseconds(250),
|
||||||
EasingFunction = new QuadraticEase()
|
EasingFunction = new QuadraticEase()
|
||||||
};
|
};
|
||||||
ShutdownSliderTransform.BeginAnimation(System.Windows.Media.TranslateTransform.XProperty, animation);
|
ShutdownSliderTransform.BeginAnimation(TranslateTransform.XProperty, animation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ShutdownWindows(object sender, RoutedEventArgs e)
|
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;
|
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;
|
FlashOverlay.BeginAnimation(OpacityProperty,
|
||||||
CaptureStatusText.Text = "Scharfstellen";
|
new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(450))
|
||||||
_focusStatusAnimationTimer.Start();
|
{
|
||||||
}
|
EasingFunction = new PowerEase { Power = 2, EasingMode = EasingMode.EaseIn }
|
||||||
|
});
|
||||||
private void StopFocusStatusAnimation()
|
|
||||||
{
|
|
||||||
_focusStatusAnimationTimer.Stop();
|
|
||||||
_focusStatusDots = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShowLatestPhotoDialogAsync()
|
private async Task ShowLatestPhotoDialogAsync()
|
||||||
@ -403,4 +370,4 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,9 @@ public class MainWindowViewModel
|
|||||||
|
|
||||||
public bool IsPhotoProcessRunning { get; private set; }
|
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
|
// Events → MainWindow subscribes and updates UI
|
||||||
public event Action<string, string>? LoadingProgressChanged; // (statusText, countText)
|
public event Action<string, string>? LoadingProgressChanged; // (statusText, countText)
|
||||||
public event Action? InitializationCompleted;
|
public event Action? InitializationCompleted;
|
||||||
@ -68,10 +71,30 @@ public class MainWindowViewModel
|
|||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called by MainWindow when the countdown timer fires and a photo should be taken.
|
/// Starts autofocus in the background. The effective timeout is capped so focus
|
||||||
/// Throws on camera error so MainWindow can show a message box.
|
/// is guaranteed to complete before the countdown timer fires.
|
||||||
/// </summary>
|
/// </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;
|
IsPhotoProcessRunning = false;
|
||||||
_cameraService.TakePhoto();
|
_cameraService.TakePhoto();
|
||||||
@ -82,6 +105,7 @@ public class MainWindowViewModel
|
|||||||
public void StartPhotoProcess()
|
public void StartPhotoProcess()
|
||||||
{
|
{
|
||||||
IsPhotoProcessRunning = true;
|
IsPhotoProcessRunning = true;
|
||||||
|
_focusTask = Task.CompletedTask;
|
||||||
DismissGalleryPrompt();
|
DismissGalleryPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user