using System.IO; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; using Microsoft.Win32; using VideoDownloader.Browser; using VideoDownloader.Core.Config; using VideoDownloader.Core.Social; namespace VideoDownloader.App; public partial class SettingsWindow : Window { public AppConfig Config { get; } private readonly AppConfig _original; private readonly Func? _clearBrowsingData; private readonly List _sections = new(); private bool _needsRestart; public SettingsWindow(AppConfig config, Func? clearBrowsingData = null) { InitializeComponent(); Config = config; _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(); 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().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"); FmtWebM.IsChecked = config.EnabledFormats.Contains("webm"); FmtMkv.IsChecked = config.EnabledFormats.Contains("mkv"); FmtM4v.IsChecked = config.EnabledFormats.Contains("m4v"); FmtMov.IsChecked = config.EnabledFormats.Contains("mov"); FmtAvi.IsChecked = config.EnabledFormats.Contains("avi"); FmtAudio.IsChecked = config.EnabledFormats.Contains("audio"); YtDlpBox.Text = config.YtDlpPath ?? ""; UseCookiesBox.IsChecked = config.UseCookiesForSocial; AutoInstallBox.IsChecked = config.AutoInstallYtDlp; AutoUpdateBox.IsChecked = config.AutoUpdateYtDlp; AutoInstallDenoBox.IsChecked = config.AutoInstallDeno; 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 ? "개인정보 보호 모드(InPrivate) — 데이터를 저장하지 않습니다" : "일반 모드 — 쿠키·세션·localStorage 를 프로필에 보존합니다"; 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 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 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().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) { if (SearchEngineHint == null) return; var engine = SearchEngines.FindByName(SearchEngineBox.SelectedItem as string); SearchEngineHint.Text = engine == null ? "주소창에 검색어를 입력하면 검색합니다" : $"주소창에 검색어 입력 시 {engine.Name}(으)로 검색합니다. `g 검색어`처럼 키워드로 즉시 전환도 가능해요"; } private void BrowseFolder(object sender, RoutedEventArgs e) { var dlg = new OpenFolderDialog(); if (Directory.Exists(FolderBox.Text)) dlg.InitialDirectory = FolderBox.Text; if (dlg.ShowDialog() == true) FolderBox.Text = dlg.FolderName; } private void BrowseFfmpeg(object sender, RoutedEventArgs e) { var dlg = new OpenFileDialog { Filter = "ffmpeg|ffmpeg.exe|실행 파일|*.exe|모든 파일|*.*" }; if (dlg.ShowDialog() == true) FfmpegBox.Text = dlg.FileName; } private void BrowseYtDlp(object sender, RoutedEventArgs e) { var dlg = new OpenFileDialog { Filter = "yt-dlp|yt-dlp.exe|실행 파일|*.exe|모든 파일|*.*" }; if (dlg.ShowDialog() == true) YtDlpBox.Text = dlg.FileName; } private async void ReinstallYtDlp_Click(object sender, RoutedEventArgs e) { var btn = sender as Button; if (btn != null) btn.IsEnabled = false; try { var path = await YtDlpRunner.DownloadAsync(Config, null, CancellationToken.None); MessageBox.Show(path != null ? $"yt-dlp 설치 완료:\n{path}" : "yt-dlp 다운로드에 실패했습니다. 네트워크를 확인하세요.", "yt-dlp", MessageBoxButton.OK, path != null ? MessageBoxImage.Information : MessageBoxImage.Warning); if (path != null) YtDlpBox.Text = ""; } finally { if (btn != null) btn.IsEnabled = true; } } private async void ClearData_Click(object sender, RoutedEventArgs e) { if (_clearBrowsingData == null) return; if (MessageBox.Show("쿠키·localStorage·캐시 등 브라우저 데이터를 삭제합니다.\n로그인이 풀릴 수 있습니다. 계속할까요?", "데이터 삭제", MessageBoxButton.OKCancel, MessageBoxImage.Warning) != MessageBoxResult.OK) return; var btn = sender as Button; if (btn != null) btn.IsEnabled = false; try { await _clearBrowsingData(); } finally { if (btn != null) btn.IsEnabled = true; } } // ---- 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(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(); } /// UC-S09 취소 시 변경 폐기 확인. 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(); } /// 화면 값 → Config 반영(검증 포함). 오류는 ValidationHint 에 표시하고 false 반환. 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(); 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 = 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(); if (FmtHls.IsChecked == true) fmts.Add("hls"); if (FmtDash.IsChecked == true) fmts.Add("dash"); if (FmtMp4.IsChecked == true) fmts.Add("mp4"); if (FmtWebM.IsChecked == true) fmts.Add("webm"); if (FmtMkv.IsChecked == true) fmts.Add("mkv"); if (FmtM4v.IsChecked == true) fmts.Add("m4v"); if (FmtMov.IsChecked == true) fmts.Add("mov"); if (FmtAvi.IsChecked == true) fmts.Add("avi"); if (FmtAudio.IsChecked == true) fmts.Add("audio"); Config.EnabledFormats = fmts; Config.YtDlpPath = string.IsNullOrWhiteSpace(YtDlpBox.Text) ? null : YtDlpBox.Text.Trim(); Config.UseCookiesForSocial = UseCookiesBox.IsChecked == true; Config.AutoInstallYtDlp = AutoInstallBox.IsChecked == true; Config.AutoUpdateYtDlp = AutoUpdateBox.IsChecked == true; Config.AutoInstallDeno = AutoInstallDenoBox.IsChecked == true; Config.DefaultSocialQuality = (DefaultQualityBox.SelectedValue as string) ?? "best"; Config.YoutubePlayerClients = string.IsNullOrWhiteSpace(YoutubeClientsBox.Text) ? null : YoutubeClientsBox.Text.Trim(); // 즉시 반영(재시작 없음 — §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; } /// Windows 시작 자동 실행(HKCU\...\Run) 등록/해제. 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 어두운 테두리 + 둥근 모서리. 테마 라이브 전환 시 재적용(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 DwmSetWindowAttribute(hwnd, 33, ref round, sizeof(int)); } catch { } } private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) { try { DragMove(); } catch { } } } private void Close_Click(object sender, RoutedEventArgs e) => Close(); [DllImport("dwmapi.dll")] private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int pvAttribute, int cbAttribute); }