"Refactor TimerControl: Improve countdown animation, simplify focus logic, and add shutter flash effect"

This commit is contained in:
iTob 2026-03-09 23:07:57 +01:00
parent b3c91da331
commit a3c9f9b719
5 changed files with 219 additions and 226 deletions

View File

@ -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>

View File

@ -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 });
}
}

View File

@ -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"

View File

@ -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
} }

View File

@ -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();
} }