UI 복잡 버전 234 RED→GREEN 완료(docs/UI_USECASES.md §A~§J): 라이브 테마 전환(DynamicResource 전환+ThemeResolver+HighContrast 테마, 재시작 없음), 디자인 토큰 체계·WCAG 대비 결함 5건 수정, 설정창 카테고리 내비/검색 재구성+AppConfig 40여 속성, 다운로드 관리자(속도/ETA/세그먼트맵/전체 일시정지-이어받기/CSV 내보내기/예약 게이트/중복 확인), 라이브러리·아카이브·작업공간·재생목록·즐겨찾기 편집 뷰, 접근성(자동화 속성 30여 개·커스텀 피어·docs/ACCESSIBILITY.md), 인터랙션(휠 클릭 새 탭·XButton 탐색·F12·영역 캡처·프로필 전환), 상태/알림(토스트·알림센터·온보딩·CrashReportDialog). 보강 테스트 35건+실창 E2E 4건(StaDispatcher 기반 — 프록시 설정이 기시작 핸들러를 건드리는 크래시 실결함 수정 포함) — 453/453 GREEN. 이전 미커밋 작업(CI 워크플로·Mdns·QueueHub·UpdateChecker·AGENTS/CLAUDE·빌드 스크립트) 일괄 포함
This commit is contained in:
parent
5a0ce730b2
commit
17f2f91152
88 changed files with 9463 additions and 1127 deletions
114
VideoDownloader.App/Services/ToastNotifier.cs
Normal file
114
VideoDownloader.App/Services/ToastNotifier.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
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;
|
||||
|
||||
/// <summary>
|
||||
/// UC-D11/UC-N02 토스트 알림 — 지정한 호스트 패널(우하단)에 카드를 띄우고
|
||||
/// 설정된 시간 후 사라지게 한다. 방해 금지(DoNotDisturb)면 표시를 억제하고 알림센터에만 쌓는다.
|
||||
/// UI 스레드 DispatcherTimer 기반이라 별도 스레드가 없다.
|
||||
/// </summary>
|
||||
public sealed class ToastNotifier
|
||||
{
|
||||
private readonly Panel _host;
|
||||
private readonly DispatcherTimer _timer;
|
||||
private readonly List<string> _history = new();
|
||||
|
||||
/// <summary>토스트 표시 시간. AppConfig.ToastDurationMs 와 연동.</summary>
|
||||
public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(4);
|
||||
|
||||
/// <summary>UC-N15 방해 금지 — true 면 토스트를 띄우지 않고 기록만.</summary>
|
||||
public bool DoNotDisturb { get; set; }
|
||||
|
||||
/// <summary>UC-N14 알림 센터용 최근 알림 이력(최신 우선, 상한 100).</summary>
|
||||
public IReadOnlyList<string> History => _history.ToList();
|
||||
|
||||
public ToastNotifier(Panel host)
|
||||
{
|
||||
_host = host;
|
||||
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
|
||||
_timer.Tick += (_, _) => Expire();
|
||||
}
|
||||
|
||||
/// <summary>토스트 표시. kind 에 따라 아이콘·테두리 색이 달라진다(success/error/info).</summary>
|
||||
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<Border>().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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue