155 lines
7.5 KiB
C#
155 lines
7.5 KiB
C#
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;
|
|
|
|
public partial class App : Application
|
|
{
|
|
private static string CrashLogPath => Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VideoDownloader", "crash.log");
|
|
|
|
// #14 글로벌 예외 핸들러: 잡히지 않은 예외를 crash.log 에 기록 + UC-N18 크래시 리포트 다이얼로그 표시.
|
|
protected override void OnStartup(StartupEventArgs e)
|
|
{
|
|
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);
|
|
|
|
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/system/highcontrast/사용자파일) + Shared(스타일) 리소스사전을 App.Resources 에 주입.
|
|
/// 주의: 반드시 색(brush) 사전을 스타일 사전보다 먼저 로드해야 Shared 의 템플릿이
|
|
/// {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();
|
|
// 어셈블리 한정 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:,,,/VideoDownloader;component/Themes/Shared.xaml", UriKind.Absolute) });
|
|
Current.Resources["EffectiveTheme"] = effective;
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
if (ex == null) return;
|
|
try
|
|
{
|
|
var dir = Path.GetDirectoryName(CrashLogPath);
|
|
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
|
File.AppendAllText(CrashLogPath,
|
|
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {ex}{Environment.NewLine}{new string('-', 60)}{Environment.NewLine}");
|
|
}
|
|
catch { }
|
|
}
|
|
}
|