releases/VideoDownloader.App/SettingsWindow.xaml.cs

588 lines
27 KiB
C#

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<Task>? _clearBrowsingData;
private readonly List<StackPanel> _sections = new();
private bool _needsRestart;
public SettingsWindow(AppConfig config, Func<Task>? 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<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");
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<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)
{
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<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();
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<string>();
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;
}
/// <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 어두운 테두리 + 둥근 모서리. 테마 라이브 전환 시 재적용(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);
}