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
|
|
@ -1,7 +1,9 @@
|
|||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using VideoDownloader.App.Core;
|
||||
using VideoDownloader.App.Services;
|
||||
|
||||
namespace VideoDownloader.App;
|
||||
|
||||
|
|
@ -10,33 +12,133 @@ public partial class App : Application
|
|||
private static string CrashLogPath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VideoDownloader", "crash.log");
|
||||
|
||||
// #14 글로벌 예외 핸들러: 잡히지 않은 예외를 crash.log 에 기록해 조용히 삼켜지는 것을 방지.
|
||||
// #14 글로벌 예외 핸들러: 잡히지 않은 예외를 crash.log 에 기록 + UC-N18 크래시 리포트 다이얼로그 표시.
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
DispatcherUnhandledException += (_, de) => { LogCrash(de.Exception); de.Handled = true; };
|
||||
DispatcherUnhandledException += (_, de) => { LogCrash(de.Exception); CrashReportDialog.Show(de.Exception); de.Handled = true; };
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, ue) => LogCrash(ue.ExceptionObject as Exception);
|
||||
TaskScheduler.UnobservedTaskException += (_, ue) => { LogCrash(ue.Exception); ue.SetObserved(); };
|
||||
base.OnStartup(e);
|
||||
|
||||
ApplyTheme(ConfigStore.Load().Theme);
|
||||
var config = ConfigStore.Load();
|
||||
// UC-A04: OS 고대비 모드가 켜져 있고 별도 선택이 없으면 고대비 테마로 시작
|
||||
if (SystemParameters.HighContrast && config.Theme is "dark" or "light" or "system")
|
||||
config.Theme = "highcontrast";
|
||||
ApplyTheme(config.Theme, config.CustomThemePath);
|
||||
ApplyAccent(config.AccentColor);
|
||||
ApplyFontSettings();
|
||||
ApplyReduceMotion(config.ReduceMotion);
|
||||
new MainWindow().Show();
|
||||
}
|
||||
|
||||
/// <summary>테마 색(dark/light) + Shared(스타일) 리소스사전을 App.Resources 에 주입.
|
||||
/// <summary>테마 색(dark/light/system/highcontrast/사용자파일) + Shared(스타일) 리소스사전을 App.Resources 에 주입.
|
||||
/// 주의: 반드시 색(brush) 사전을 스타일 사전보다 먼저 로드해야 Shared 의 템플릿이
|
||||
/// {StaticResource GlassBg} 등을 해석할 수 있다.</summary>
|
||||
public static void ApplyTheme(string theme)
|
||||
/// {DynamicResource GlassBg} 등을 해석할 수 있다. 인덱스 0 자리를 교체해 라이브 전환이 동작한다(UC-T04).</summary>
|
||||
public static void ApplyTheme(string theme, string? customThemePath = null)
|
||||
{
|
||||
var (effective, customFile) = Services.ThemeResolver.Resolve(theme, customThemePath);
|
||||
var dicts = Current.Resources.MergedDictionaries;
|
||||
dicts.Clear();
|
||||
var themeSrc = theme.Equals("light", StringComparison.OrdinalIgnoreCase)
|
||||
? "pack://application:,,,/Themes/Light.xaml" : "pack://application:,,,/Themes/Dark.xaml";
|
||||
// 어셈블리 한정 pack URI — 테스트 호스트(타 어셈블리 Application)에서도 App 의 테마를 찾는다.
|
||||
var themeSrc = effective switch
|
||||
{
|
||||
"light" => "pack://application:,,,/VideoDownloader;component/Themes/Light.xaml",
|
||||
"highcontrast" => "pack://application:,,,/VideoDownloader;component/Themes/HighContrast.xaml",
|
||||
"custom" when customFile != null => customFile, // UC-T12 사용자 테마 파일(절대경로)
|
||||
_ => "pack://application:,,,/VideoDownloader;component/Themes/Dark.xaml",
|
||||
};
|
||||
dicts.Add(new ResourceDictionary { Source = new Uri(themeSrc, UriKind.Absolute) });
|
||||
dicts.Add(new ResourceDictionary { Source = new Uri("pack://application:,,,/Themes/Shared.xaml", UriKind.Absolute) });
|
||||
dicts.Add(new ResourceDictionary { Source = new Uri("pack://application:,,,/VideoDownloader;component/Themes/Shared.xaml", UriKind.Absolute) });
|
||||
Current.Resources["EffectiveTheme"] = effective;
|
||||
}
|
||||
|
||||
/// <summary>현재 적용된 테마가 라이트인지 (DWM 타이틀바 색 연동용).</summary>
|
||||
public static bool IsLightTheme => ConfigStore.Load().Theme.Equals("light", StringComparison.OrdinalIgnoreCase);
|
||||
/// <summary>현재 적용된 테마(dark/light/highcontrast/custom) — DWM 타이틀바·판정용.</summary>
|
||||
public static string CurrentTheme => Current.Resources["EffectiveTheme"] as string ?? "dark";
|
||||
|
||||
/// <summary>현재 적용된 테마가 밝은 계열인지(DWM 타이틀바 색 연동용).</summary>
|
||||
public static bool IsLightTheme => CurrentTheme is "light" or "custom";
|
||||
|
||||
/// <summary>UC-T08 사용자 강조색 적용 — 현재 테마 사전의 AccentBrush/Gradient/Fade/AccentText 를
|
||||
/// 파생색으로 교체한다. DynamicResource 로 물린 모든 컨트롤(버튼·진행바)에 즉시 반영된다.</summary>
|
||||
public static void ApplyAccent(string? hex)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hex) || !TryParseColor(hex, out var color)) return;
|
||||
var dict = Current.Resources.MergedDictionaries.FirstOrDefault(d => d.Contains("AccentBrush"));
|
||||
if (dict == null) return;
|
||||
var base_ = Color.FromArgb(0xFF, color.R, color.G, color.B);
|
||||
dict["AccentBrush"] = new SolidColorBrush(base_);
|
||||
dict["AccentTextBrush"] = new SolidColorBrush(Lighten(base_, 0.12));
|
||||
var grad = new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new Point(0, 0),
|
||||
EndPoint = new Point(1, 1),
|
||||
GradientStops =
|
||||
{
|
||||
new GradientStop(base_, 0),
|
||||
new GradientStop(Lighten(base_, 0.18), 1),
|
||||
},
|
||||
};
|
||||
dict["AccentGradient"] = grad;
|
||||
var fade = new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new Point(0, 0),
|
||||
EndPoint = new Point(0, 1),
|
||||
GradientStops =
|
||||
{
|
||||
new GradientStop(Color.FromArgb(0x66, base_.R, base_.G, base_.B), 0),
|
||||
new GradientStop(Color.FromArgb(0x00, base_.R, base_.G, base_.B), 1),
|
||||
},
|
||||
};
|
||||
dict["AccentFadeBrush"] = fade;
|
||||
}
|
||||
|
||||
/// <summary>UC-F16 폰트 설정 즉시 적용 — AppConfig 의 폰트/크기/확대 배율으로 UiFont·FontSize.Base 토큰 교체.</summary>
|
||||
public static void ApplyFontSettings()
|
||||
{
|
||||
var c = ConfigStore.Load();
|
||||
var stack = string.IsNullOrWhiteSpace(c.UiFontFamily)
|
||||
? "Pretendard, Segoe UI Variable Text, Segoe UI"
|
||||
: c.UiFontFamily.Trim();
|
||||
var fallback = string.IsNullOrWhiteSpace(c.UiFontKoreanFallback) ? "Malgun Gothic" : c.UiFontKoreanFallback.Trim();
|
||||
Current.Resources["UiFont"] = new FontFamily($"{stack}, {fallback}");
|
||||
var scale = Math.Clamp(c.UiFontScale <= 0 ? 1.0 : c.UiFontScale, 0.5, 2.0); // UC-A13 최대 200%
|
||||
var size = Math.Clamp(c.UiFontSize * scale, 9, 40);
|
||||
Current.Resources["FontSize.Base"] = size;
|
||||
Current.Resources["FontSize.Small"] = size - 1.5;
|
||||
Current.Resources["FontSize.Large"] = size + 3;
|
||||
Current.Resources["FontSize.XLarge"] = size + 9;
|
||||
}
|
||||
|
||||
/// <summary>UC-A16/L21 애니메이션 축소 — Duration 토큰을 0으로 만들어 전환을 즉시 완결시킨다.</summary>
|
||||
public static void ApplyReduceMotion(bool reduce)
|
||||
{
|
||||
Current.Resources["ReduceMotion"] = reduce;
|
||||
Current.Resources["Duration.Fast"] = new System.Windows.Duration(TimeSpan.Zero);
|
||||
Current.Resources["Duration.Normal"] = new System.Windows.Duration(reduce ? TimeSpan.Zero : TimeSpan.FromSeconds(0.3));
|
||||
if (!reduce) Current.Resources["Duration.Fast"] = new System.Windows.Duration(TimeSpan.FromSeconds(0.15));
|
||||
}
|
||||
|
||||
private static bool TryParseColor(string hex, out Color color)
|
||||
{
|
||||
hex = hex.Trim().TrimStart('#');
|
||||
if (hex.Length == 6)
|
||||
{
|
||||
try
|
||||
{
|
||||
color = Color.FromRgb(
|
||||
Convert.ToByte(hex[..2], 16), Convert.ToByte(hex[2..4], 16), Convert.ToByte(hex[4..6], 16));
|
||||
return true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
color = Colors.Transparent;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Color Lighten(Color c, double amount) => Color.FromRgb(
|
||||
(byte)Math.Min(255, c.R + (255 - c.R) * amount),
|
||||
(byte)Math.Min(255, c.G + (255 - c.G) * amount),
|
||||
(byte)Math.Min(255, c.B + (255 - c.B) * amount));
|
||||
|
||||
private static void LogCrash(Exception? ex)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue