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:
Yun Chan 2026-08-17 15:54:53 +09:00
parent 5a0ce730b2
commit 17f2f91152
88 changed files with 9463 additions and 1127 deletions

View file

@ -1,3 +1,4 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
@ -13,25 +14,106 @@ namespace VideoDownloader.App;
public partial class SettingsWindow : Window
{
public AppConfig Config { get; }
private readonly string _origTheme;
private readonly AppConfig _original;
private readonly Func<Task>? _clearBrowsingData;
public bool ThemeChanged { get; private set; }
private readonly List<StackPanel> _sections = new();
private bool _needsRestart;
public SettingsWindow(AppConfig config, Func<Task>? clearBrowsingData = null)
{
InitializeComponent();
Config = config;
_origTheme = config.Theme;
_original = CloneConfig(config);
_clearBrowsingData = clearBrowsingData;
LoadValues();
// UC-S02 카테고리 내비게이션 — 섹션 제목과 1:1
var names = new[] { "외관", "글꼴", "일반", "다운로드", "알림", "접근성", "브라우저", "고급", "단축키", "실험적", "정보" };
SettingsNavList.ItemsSource = names;
_sections.Add(SectionAppearance);
_sections.Add(SectionFont);
_sections.Add(SectionGeneral);
_sections.Add(SectionDownload);
_sections.Add(SectionNotify);
_sections.Add(SectionAccessibility);
_sections.Add(SectionBrowser);
if (AdvancedSection.Content is StackPanel advanced) _sections.Add(advanced);
_sections.Add(SectionShortcuts);
_sections.Add(SectionExperimental);
_sections.Add(SectionAbout);
// UC-S20 버전 정보
VersionText.Text = $"Video Downloader {CurrentVersion()} · {MainWindow.ProfileFolder}";
// UC-S08 Ctrl+S 저장
PreviewKeyDown += (_, e) =>
{
if (e.Key == Key.S && Keyboard.Modifiers == ModifierKeys.Control) { Save(); DialogResult = true; e.Handled = true; }
};
}
private void LoadValues()
{
var config = Config;
FolderBox.Text = config.OutputFolder;
HomeBox.Text = config.HomeUrl;
ConcurrencyBox.Text = config.Concurrency.ToString();
MinSizeBox.Text = config.MinSizeMB.ToString();
MaxConcurrentBox.Text = config.MaxConcurrentDownloads.ToString();
ThemeLightBox.IsChecked = config.Theme.Equals("light", StringComparison.OrdinalIgnoreCase);
RetriesBox.Text = config.Retries.ToString();
FfmpegBox.Text = config.FfmpegPath ?? "";
// §A 테마 — 즉시 적용(UC-T03): 재시작 없음
ThemeModeBox.SelectedValue = config.Theme;
if (ThemeModeBox.SelectedIndex < 0) ThemeModeBox.SelectedValue = "dark";
var accent = config.AccentColor;
AccentBlue.IsChecked = accent == "#60A5FA" || accent == "#2563EB";
AccentGreen.IsChecked = accent == "#34D399" || accent == "#15803D";
AccentPink.IsChecked = accent == "#F472B6" || accent == "#DB2777";
AccentDefault.IsChecked = !AccentBlue.IsChecked.Value && !AccentGreen.IsChecked.Value && !AccentPink.IsChecked.Value;
AccentColorBox.Text = accent ?? "";
CustomThemeBox.Text = config.CustomThemePath ?? "";
UiDensityBox.SelectedValue = config.UiDensity;
if (UiDensityBox.SelectedIndex < 0) UiDensityBox.SelectedValue = "normal";
UiScaleSlider.Value = Math.Clamp(config.UiScale, 1.0, 2.0);
UiScaleLabel.Text = $"{Math.Round(UiScaleSlider.Value * 100)}%";
UiFontScaleSlider.Value = Math.Clamp(config.UiFontScale <= 0 ? 1.0 : config.UiFontScale, 1.0, 2.0);
UiFontFamilyBox.Text = config.UiFontFamily ?? "";
UiFontKoreanFallbackBox.Text = config.UiFontKoreanFallback ?? "Malgun Gothic";
FontSizeBox.SelectedValue = config.UiFontSize.ToString("0");
if (FontSizeBox.SelectedIndex < 0) FontSizeBox.SelectedValue = "13";
LanguageBox.SelectedValue = config.Language;
if (LanguageBox.SelectedIndex < 0) LanguageBox.SelectedValue = "ko";
AutoStartBox.IsChecked = config.AutoStart;
MinimizeToTrayBox.IsChecked = config.MinimizeToTray;
MaxSpeedBox.Text = config.MaxDownloadSpeedKbps.ToString();
AutoClearCompletedBox.IsChecked = config.AutoClearCompleted;
OpenFolderOnCompleteBox.IsChecked = config.OpenFolderOnComplete;
SubtitleLangBox.SelectedValue = config.SubtitleLanguage;
if (SubtitleLangBox.SelectedIndex < 0) SubtitleLangBox.SelectedItem = SubtitleLangBox.Items.OfType<ComboBoxItem>().First();
ScheduleBox.Text = config.DownloadScheduleWindow ?? "";
NotifySoundBox.IsChecked = config.NotifySound;
DoNotDisturbBox.IsChecked = config.DoNotDisturb;
PauseOnPowerSaverBox.IsChecked = config.PauseOnPowerSaver;
ToastDurationBox.Text = (config.ToastDurationMs / 1000.0).ToString("0");
ReduceMotionBox.IsChecked = config.ReduceMotion;
ShowCaptionsBox.IsChecked = config.ShowCaptions;
FocusSelectsAddressBox.IsChecked = config.FocusSelectsAddress;
PasteAndGoBox.IsChecked = config.PasteAndGo;
TabWheelSwitchBox.IsChecked = config.TabWheelSwitch;
TabStripPositionBox.SelectedValue = config.TabStripPosition;
if (TabStripPositionBox.SelectedIndex < 0) TabStripPositionBox.SelectedValue = "top";
ConfirmCloseMultipleTabsBox.IsChecked = config.ConfirmCloseMultipleTabs;
BookmarkBarVisibleBox.IsChecked = config.BookmarkBarVisible;
FmtHls.IsChecked = config.EnabledFormats.Contains("hls");
FmtDash.IsChecked = config.EnabledFormats.Contains("dash");
FmtMp4.IsChecked = config.EnabledFormats.Contains("mp4");
@ -50,6 +132,11 @@ public partial class SettingsWindow : Window
DefaultQualityBox.SelectedValue = config.DefaultSocialQuality;
if (DefaultQualityBox.SelectedIndex < 0) DefaultQualityBox.SelectedIndex = 0;
YoutubeClientsBox.Text = config.YoutubePlayerClients ?? "";
UserAgentBox.Text = config.UserAgent;
ProxyBox.Text = config.ProxyServer;
RestoreTabsSuspendedBox.IsChecked = config.RestoreTabsSuspended;
TabTitleMultilineBox.IsChecked = config.TabTitleMultiline;
ProfilePathBox.Text = MainWindow.ProfileFolder;
ProfileModeText.Text = config.PrivateMode
@ -59,6 +146,150 @@ public partial class SettingsWindow : Window
SearchEngineBox.ItemsSource = SearchEngines.Presets.Select(e => e.Name).ToList();
SearchEngineBox.SelectedItem = SearchEngines.FindByName(config.SearchEngine)?.Name ?? "Google";
if (SearchEngineBox.SelectedIndex < 0) SearchEngineBox.SelectedIndex = 0;
UpdateRepoBox.Text = config.UpdateRepo;
// UC-F14 폰트 미리보기 — 현재 설정 반영
RefreshFontPreview();
}
// ---- UC-S01 검색: 섹션 제목·내용에 검색어가 없으면 숨김 ----
private void SettingsSearchBox_TextChanged(object sender, TextChangedEventArgs e)
{
var q = SettingsSearchBox.Text?.Trim() ?? "";
foreach (var s in _sections)
{
var text = SectionText(s);
s.Visibility = q.Length == 0 || text.Contains(q, StringComparison.OrdinalIgnoreCase)
? Visibility.Visible : Visibility.Collapsed;
}
}
private static string SectionText(FrameworkElement section)
{
var sb = new System.Text.StringBuilder();
sb.Append(section.Tag ?? "");
CollectText(section, sb);
return sb.ToString();
}
private static void CollectText(DependencyObject d, System.Text.StringBuilder sb)
{
foreach (var child in System.Windows.LogicalTreeHelper.GetChildren(d))
{
switch (child)
{
case TextBlock tb: sb.AppendLine(tb.Text); break;
case TextBox tx: sb.AppendLine(tx.Text); break;
case DependencyObject dep: CollectText(dep, sb); break;
}
}
}
// ---- UC-S02 내비게이션: 섹션 스크롤 ----
private void SettingsNav_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (SettingsNavList.SelectedIndex < 0 || SettingsNavList.SelectedIndex >= _sections.Count) return;
_sections[SettingsNavList.SelectedIndex].BringIntoView();
}
// ---- §A 테마 라이브 적용(UC-T03/T04) ----
private void ThemeModeBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ThemeModeBox?.SelectedValue == null) return;
var theme = ThemeModeBox.SelectedValue as string ?? "dark";
Config.Theme = theme;
App.ApplyTheme(theme, Config.CustomThemePath);
App.ApplyAccent(Config.AccentColor);
RefreshTitleBarChrome();
(Owner as MainWindow)?.UpdateTitleBarTheme();
}
private void Accent_Checked(object sender, RoutedEventArgs e)
{
if (AccentColorBox == null) return;
if (ReferenceEquals(sender, AccentDefault)) AccentColorBox.Text = "";
else if (ReferenceEquals(sender, AccentBlue)) AccentColorBox.Text = "#2563EB";
else if (ReferenceEquals(sender, AccentGreen)) AccentColorBox.Text = "#15803D";
else if (ReferenceEquals(sender, AccentPink)) AccentColorBox.Text = "#DB2777";
}
private void BrowseThemeFile(object sender, RoutedEventArgs e)
{
var dlg = new OpenFileDialog { Filter = "테마 파일|*.xaml" };
if (dlg.ShowDialog() == true)
{
CustomThemeBox.Text = dlg.FileName;
Config.CustomThemePath = dlg.FileName;
App.ApplyTheme(Config.Theme, dlg.FileName); // 즉시 미리보기
RefreshTitleBarChrome();
}
}
// ---- §B/§C 라이브 미리보기 ----
private void RefreshFontPreview()
{
var size = double.TryParse(FontSizeBox?.SelectedValue as string, out var s) ? s : 13;
FontPreviewText.FontSize = size * Math.Clamp(UiFontScaleSlider.Value, 1.0, 2.0);
}
private void UiScaleSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (UiScaleLabel != null) UiScaleLabel.Text = $"{Math.Round(e.NewValue * 100)}%";
(Owner as MainWindow)?.ApplyUiScale(e.NewValue); // UC-L25 라이브 미리보기
}
private void UiFontScaleSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) => RefreshFontPreview();
// ---- DEPLOYMENT_PLAN §6 v1 업데이트 확인 ----
internal async void CheckUpdate_Click(object? sender, RoutedEventArgs e)
{
var repo = UpdateRepoBox.Text?.Trim() ?? "";
if (repo.Length == 0 || !repo.Contains('/'))
{
UpdateResultText.Text = "저장소를 owner/repo 형식으로 입력하세요 (예: user/videodownloader)";
return;
}
var btn = sender as Button;
if (btn != null) btn.IsEnabled = false;
try
{
var current = CurrentVersion();
var rid = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "win-arm64" : "win-x64";
var result = await new VideoDownloader.Core.Update.UpdateChecker().CheckAsync(repo, current, rid);
if (result.IsNewer)
{
UpdateResultText.Text = $"새 버전 {result.LatestVersion} (현재 {current})";
var open = MessageBox.Show(
$"새 버전 {result.LatestVersion} 이(가) 있습니다 (현재 {current}).\n릴리스 페이지를 열까요?",
"업데이트", MessageBoxButton.YesNo, MessageBoxImage.Information);
if (open == MessageBoxResult.Yes)
{
var url = result.Asset?.DownloadUrl ?? result.ReleaseUrl;
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true });
}
}
else
{
UpdateResultText.Text = $"최신 버전입니다 (현재 {current}, 최신 {result.LatestVersion})";
}
}
catch (Exception ex) when (ex is System.Net.Http.HttpRequestException or TaskCanceledException)
{
UpdateResultText.Text = $"확인 실패: {ex.Message}";
}
finally { if (btn != null) btn.IsEnabled = true; }
}
private static string CurrentVersion()
{
var asm = System.Reflection.Assembly.GetExecutingAssembly();
var info = asm.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
.OfType<System.Reflection.AssemblyInformationalVersionAttribute>().FirstOrDefault()?.InformationalVersion;
// 소스 링크 해시(+abc...) 는 버전 비교에서 제외
if (!string.IsNullOrEmpty(info)) return info.Split('+')[0];
return asm.GetName().Version?.ToString() ?? "0.0.0";
}
private void SearchEngineBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
@ -117,17 +348,149 @@ public partial class SettingsWindow : Window
finally { if (btn != null) btn.IsEnabled = true; }
}
private void Ok_Click(object sender, RoutedEventArgs e)
// ---- UC-S04/S05/S06 기본값·내보내기·가져오기 ----
private void ResetDefaults_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show("모든 설정을 기본값으로 되돌릴까요? 저장 폴더·토큰도 초기화됩니다.",
"기본값 복원", MessageBoxButton.OKCancel, MessageBoxImage.Warning) != MessageBoxResult.OK) return;
Config.ResetToDefaults();
LoadValues();
ValidationHint.Text = "기본값으로 되돌렸습니다 — 확인을 누르면 저장됩니다";
}
private void ExportSettings_Click(object sender, RoutedEventArgs e)
{
Save(); // 화면 값 → Config 반영 후 내보냄
var dlg = new SaveFileDialog { Filter = "설정 JSON|*.json", FileName = "videodownloader-settings.json" };
if (dlg.ShowDialog() != true) return;
File.WriteAllText(dlg.FileName, System.Text.Json.JsonSerializer.Serialize(Config, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));
}
private void ImportSettings_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFileDialog { Filter = "설정 JSON|*.json" };
if (dlg.ShowDialog() != true) return;
try
{
var imported = System.Text.Json.JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(dlg.FileName));
if (imported == null) throw new InvalidOperationException("빈 설정 파일");
ImportInto(imported);
LoadValues();
ValidationHint.Text = "설정을 가져왔습니다 — 확인을 누르면 저장됩니다";
}
catch (Exception ex)
{
ValidationHint.Text = $"가져오기 실패: {ex.Message}";
}
}
private void ImportInto(AppConfig src)
{
foreach (var prop in typeof(AppConfig).GetProperties().Where(p => p.CanRead && p.CanWrite))
{
if (prop.Name == nameof(AppConfig.RemoteAccessToken)) continue; // 토큰은 현재 기기 값 유지
try { prop.SetValue(Config, prop.GetValue(src)); } catch { }
}
}
private void RestartNow_Click(object sender, RoutedEventArgs e)
{
Save();
MainWindow.RestartApp();
}
/// <summary>UC-S09 취소 시 변경 폐기 확인.</summary>
public bool ConfirmDiscardChanges()
{
if (!IsDirty()) return true;
return MessageBox.Show(this, "저장하지 않은 변경 사항이 있습니다.\n폐기하고 닫을까요?",
"변경 사항", MessageBoxButton.OKCancel, MessageBoxImage.Warning) == MessageBoxResult.OK;
}
private bool IsDirty() => !System.Text.Json.JsonSerializer.Serialize(Config).Equals(
System.Text.Json.JsonSerializer.Serialize(_original));
private static AppConfig CloneConfig(AppConfig c)
{
var clone = new AppConfig();
foreach (var prop in typeof(AppConfig).GetProperties().Where(p => p.CanRead && p.CanWrite))
try { prop.SetValue(clone, prop.GetValue(c)); } catch { }
return clone;
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
if (ConfirmDiscardChanges()) Close();
}
/// <summary>화면 값 → Config 반영(검증 포함). 오류는 ValidationHint 에 표시하고 false 반환.</summary>
private bool Save()
{
ValidationHint.Text = "";
if (!int.TryParse(ConcurrencyBox.Text, out var c) || c < 1) return Invalid("세그먼트 동시 수는 1 이상의 숫자");
if (!int.TryParse(MaxConcurrentBox.Text, out var mc) || mc < 1) return Invalid("최대 동시 항목은 1 이상의 숫자");
if (!int.TryParse(RetriesBox.Text, out var retries) || retries < 1) return Invalid("재시도 횟수는 1 이상의 숫자");
if (!long.TryParse(MinSizeBox.Text, out var m) || m < 0) return Invalid("최소 크기는 0 이상의 숫자");
if (!int.TryParse(MaxSpeedBox.Text, out var speed) || speed < 0) return Invalid("속도 상한은 0 이상의 숫자");
if (!double.TryParse(ToastDurationBox.Text, out var toastSec) || toastSec < 0.5) return Invalid("토스트 시간은 0.5초 이상");
if (!string.IsNullOrWhiteSpace(ScheduleBox.Text) && !Services.DownloadScheduler.TryParse(ScheduleBox.Text.Trim(), out _, out _))
return Invalid("예약 시간대는 HH:mm-HH:mm 형식(예: 02:00-08:00)");
var accent = AccentColorBox.Text?.Trim();
if (!string.IsNullOrEmpty(accent) &&
!(accent.Length == 7 && accent.StartsWith('#') && accent[1..].All(ch => Uri.IsHexDigit(ch))))
return Invalid("강조색은 #RRGGBB 형식");
Config.OutputFolder = FolderBox.Text?.Trim() ?? "";
Config.HomeUrl = string.IsNullOrWhiteSpace(HomeBox.Text) ? "https://www.google.com" : HomeBox.Text.Trim();
if (int.TryParse(ConcurrencyBox.Text, out var c)) Config.Concurrency = Math.Max(1, c);
if (long.TryParse(MinSizeBox.Text, out var m) && m >= 0) Config.MinSizeMB = m;
Config.Concurrency = Math.Max(1, c);
Config.MaxConcurrentDownloads = Math.Max(1, mc);
Config.Retries = Math.Max(1, retries);
Config.MinSizeMB = Math.Max(0, m);
Config.MaxDownloadSpeedKbps = speed;
Config.FfmpegPath = string.IsNullOrWhiteSpace(FfmpegBox.Text) ? null : FfmpegBox.Text.Trim();
Config.Theme = ThemeLightBox.IsChecked == true ? "light" : "dark";
if (int.TryParse(MaxConcurrentBox.Text, out var mc)) Config.MaxConcurrentDownloads = Math.Max(1, mc);
ThemeChanged = !Config.Theme.Equals(_origTheme, StringComparison.OrdinalIgnoreCase);
Config.Theme = ThemeModeBox.SelectedValue as string ?? "dark";
Config.AccentColor = string.IsNullOrEmpty(accent) ? null : accent;
Config.CustomThemePath = string.IsNullOrWhiteSpace(CustomThemeBox.Text) ? null : CustomThemeBox.Text.Trim();
Config.UiDensity = UiDensityBox.SelectedValue as string ?? "normal";
Config.UiScale = Math.Clamp(UiScaleSlider.Value, 1.0, 2.0);
Config.UiFontScale = Math.Clamp(UiFontScaleSlider.Value, 1.0, 2.0);
Config.UiFontFamily = UiFontFamilyBox.Text?.Trim() ?? "";
Config.UiFontKoreanFallback = string.IsNullOrWhiteSpace(UiFontKoreanFallbackBox.Text) ? "Malgun Gothic" : UiFontKoreanFallbackBox.Text.Trim();
Config.UiFontSize = double.TryParse(FontSizeBox.SelectedValue as string, out var fs) ? fs : 13;
Config.Language = LanguageBox.SelectedValue as string ?? "ko";
Config.AutoStart = AutoStartBox.IsChecked == true;
Config.MinimizeToTray = MinimizeToTrayBox.IsChecked == true;
Config.SearchEngine = SearchEngineBox.SelectedItem as string ?? "Google";
Config.UpdateRepo = UpdateRepoBox.Text?.Trim() ?? "";
Config.UserAgent = string.IsNullOrWhiteSpace(UserAgentBox.Text)
? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
: UserAgentBox.Text.Trim();
var proxyChanged = Config.ProxyServer != (ProxyBox.Text?.Trim() ?? "");
Config.ProxyServer = ProxyBox.Text?.Trim() ?? "";
Config.AutoClearCompleted = AutoClearCompletedBox.IsChecked == true;
Config.OpenFolderOnComplete = OpenFolderOnCompleteBox.IsChecked == true;
Config.SubtitleLanguage = (SubtitleLangBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "";
Config.DownloadScheduleWindow = string.IsNullOrWhiteSpace(ScheduleBox.Text) ? null : ScheduleBox.Text.Trim();
Config.NotifySound = NotifySoundBox.IsChecked == true;
Config.DoNotDisturb = DoNotDisturbBox.IsChecked == true;
Config.PauseOnPowerSaver = PauseOnPowerSaverBox.IsChecked == true;
Config.ToastDurationMs = (int)(toastSec * 1000);
Config.ReduceMotion = ReduceMotionBox.IsChecked == true;
Config.ShowCaptions = ShowCaptionsBox.IsChecked == true;
Config.FocusSelectsAddress = FocusSelectsAddressBox.IsChecked == true;
Config.PasteAndGo = PasteAndGoBox.IsChecked == true;
Config.TabWheelSwitch = TabWheelSwitchBox.IsChecked == true;
Config.TabStripPosition = TabStripPositionBox.SelectedValue as string ?? "top";
Config.ConfirmCloseMultipleTabs = ConfirmCloseMultipleTabsBox.IsChecked == true;
Config.BookmarkBarVisible = BookmarkBarVisibleBox.IsChecked == true;
Config.RestoreTabsSuspended = RestoreTabsSuspendedBox.IsChecked == true;
Config.TabTitleMultiline = TabTitleMultilineBox.IsChecked == true;
var fmts = new List<string>();
if (FmtHls.IsChecked == true) fmts.Add("hls");
@ -148,18 +511,63 @@ public partial class SettingsWindow : Window
Config.AutoInstallDeno = AutoInstallDenoBox.IsChecked == true;
Config.DefaultSocialQuality = (DefaultQualityBox.SelectedValue as string) ?? "best";
Config.YoutubePlayerClients = string.IsNullOrWhiteSpace(YoutubeClientsBox.Text) ? null : YoutubeClientsBox.Text.Trim();
Config.SearchEngine = SearchEngineBox.SelectedItem as string ?? "Google";
// 즉시 반영(재시작 없음 — §A/§B/§G)
App.ApplyFontSettings();
App.ApplyReduceMotion(Config.ReduceMotion);
App.ApplyAccent(Config.AccentColor);
// UC-S15 자동 실행 등록(HKCU Run)
ApplyAutoStart(Config.AutoStart);
// UC-S03 재시작 배너 — 프록시가 바뀌었으면 알림
if (proxyChanged) _needsRestart = true;
RestartBanner.Visibility = _needsRestart ? Visibility.Visible : Visibility.Collapsed;
return true;
}
private bool Invalid(string message)
{
ValidationHint.Text = message;
return false;
}
/// <summary>Windows 시작 자동 실행(HKCU\...\Run) 등록/해제.</summary>
private static void ApplyAutoStart(bool enable)
{
try
{
using var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run");
var name = "VideoDownloader";
if (enable)
{
var exe = Environment.ProcessPath;
if (!string.IsNullOrEmpty(exe)) key.SetValue(name, $"\"{exe}\"");
}
else key.DeleteValue(name, throwOnMissingValue: false);
}
catch { }
}
private void Ok_Click(object sender, RoutedEventArgs e)
{
if (!Save()) return;
DialogResult = true;
}
// 보더리스: Win11 어두운 테두리 + 둥근 모서리
// 보더리스: Win11 어두운 테두리 + 둥근 모서리. 테마 라이브 전환 시 재적용(RefreshTitleBarChrome).
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
RefreshTitleBarChrome();
}
internal void RefreshTitleBarChrome()
{
try
{
var hwnd = new WindowInteropHelper(this).Handle;
if (hwnd == IntPtr.Zero) return;
int dark = App.IsLightTheme ? 0 : 1; // DWMWA_USE_IMMERSIVE_DARK_MODE (라이트 테마면 밝게)
DwmSetWindowAttribute(hwnd, 20, ref dark, sizeof(int));
int round = 2; // DWMWCP_ROUND