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,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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue