using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Effects; using System.Windows.Threading; namespace VideoDownloader.App.Services; /// /// UC-D11/UC-N02 토스트 알림 — 지정한 호스트 패널(우하단)에 카드를 띄우고 /// 설정된 시간 후 사라지게 한다. 방해 금지(DoNotDisturb)면 표시를 억제하고 알림센터에만 쌓는다. /// UI 스레드 DispatcherTimer 기반이라 별도 스레드가 없다. /// public sealed class ToastNotifier { private readonly Panel _host; private readonly DispatcherTimer _timer; private readonly List _history = new(); /// 토스트 표시 시간. AppConfig.ToastDurationMs 와 연동. public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(4); /// UC-N15 방해 금지 — true 면 토스트를 띄우지 않고 기록만. public bool DoNotDisturb { get; set; } /// UC-N14 알림 센터용 최근 알림 이력(최신 우선, 상한 100). public IReadOnlyList History => _history.ToList(); public ToastNotifier(Panel host) { _host = host; _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; _timer.Tick += (_, _) => Expire(); } /// 토스트 표시. kind 에 따라 아이콘·테두리 색이 달라진다(success/error/info). public void Show(string message, string kind = "info") { _history.Insert(0, $"[{DateTime.Now:HH:mm}] {message}"); if (_history.Count > 100) _history.RemoveAt(_history.Count - 100); if (DoNotDisturb) return; var border = new Border { Background = (Brush)Application.Current.FindResource("CardBg"), BorderBrush = kind switch { "success" => (Brush)Application.Current.FindResource("SuccessBrush"), "error" => (Brush)Application.Current.FindResource("ErrorBrush"), _ => (Brush)Application.Current.FindResource("AccentBrush"), }, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 8, 0, 0), MaxWidth = 340, Effect = new DropShadowEffect { Opacity = 0.4, BlurRadius = 16, ShadowDepth = 4 }, Tag = DateTime.UtcNow.Add(Duration), }; var icon = kind switch { "success" => "\uE73E", "error" => "\uE7BA", _ => "\uE946", }; var panel = new StackPanel { Orientation = Orientation.Horizontal }; panel.Children.Add(new TextBlock { Text = icon, FontFamily = (FontFamily)Application.Current.FindResource("IconFont"), FontSize = 15, Foreground = border.BorderBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 9, 0), }); panel.Children.Add(new TextBlock { Text = message, Foreground = (Brush)Application.Current.FindResource("TextPrimary"), FontSize = 12.5, TextWrapping = TextWrapping.Wrap, VerticalAlignment = VerticalAlignment.Center, }); border.Child = panel; border.MouseLeftButtonUp += (_, _) => Collapse(border); border.Opacity = 0; _host.Children.Add(border); border.Loaded += (_, _) => { var anim = new DoubleAnimation(0, 1, (System.Windows.Duration)Application.Current.FindResource("Duration.Fast")); border.BeginAnimation(UIElement.OpacityProperty, anim); }; if (!_timer.IsEnabled) _timer.Start(); } private void Expire() { var now = DateTime.UtcNow; foreach (var child in _host.Children.OfType().ToList()) { if (child.Tag is DateTime until && now >= until) Collapse(child); } if (_host.Children.Count == 0) _timer.Stop(); } private void Collapse(Border border) { var anim = new DoubleAnimation(1, 0, (System.Windows.Duration)Application.Current.FindResource("Duration.Fast")); anim.Completed += (_, _) => _host.Children.Remove(border); border.BeginAnimation(UIElement.OpacityProperty, anim); } }