using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using Paca.Core.Config; namespace Paca.App.ViewModels; /// /// 설정 화면의 Input SSOT (Single Source of Truth) 뷰모델. /// 원본 AppConfig와의 비교를 통한 실시간 Dirty 추적, 유효성 검사, 라이브 프리뷰, 초기화/가져오기/내보내기 일원화 관리. /// public sealed class SettingsViewModel : INotifyPropertyChanged { private readonly AppConfig _originalConfig; private readonly Dictionary _originalValues = new(StringComparer.Ordinal); private readonly Dictionary _errors = new(StringComparer.Ordinal); public event PropertyChangedEventHandler? PropertyChanged; public event Action? LivePreviewRequested; public SettingsViewModel(AppConfig config) { _originalConfig = config; LoadFromConfig(config, isOriginalSnapshot: true); } #region Input Properties (SSOT) // 1. 외관 & 테마 private string _theme = "dark"; public string Theme { get => _theme; set { if (Set(ref _theme, value)) OnSettingChanged(nameof(Theme), true); } } private string? _accentColor; public string? AccentColor { get => _accentColor; set { if (Set(ref _accentColor, value)) OnSettingChanged(nameof(AccentColor), true); } } private string? _customThemePath; public string? CustomThemePath { get => _customThemePath; set { if (Set(ref _customThemePath, value)) OnSettingChanged(nameof(CustomThemePath), true); } } private string _uiDensity = "normal"; public string UiDensity { get => _uiDensity; set { if (Set(ref _uiDensity, value)) OnSettingChanged(nameof(UiDensity), true); } } private double _uiScale = 1.0; public double UiScale { get => _uiScale; set { if (Set(ref _uiScale, Math.Clamp(value, 1.0, 2.0))) OnSettingChanged(nameof(UiScale), true); } } private double _uiFontScale = 1.0; public double UiFontScale { get => _uiFontScale; set { if (Set(ref _uiFontScale, Math.Clamp(value, 1.0, 2.0))) OnSettingChanged(nameof(UiFontScale), true); } } private string _uiFontFamily = ""; public string UiFontFamily { get => _uiFontFamily; set { if (Set(ref _uiFontFamily, value)) OnSettingChanged(nameof(UiFontFamily), true); } } private string _uiFontKoreanFallback = "Malgun Gothic"; public string UiFontKoreanFallback { get => _uiFontKoreanFallback; set { if (Set(ref _uiFontKoreanFallback, value)) OnSettingChanged(nameof(UiFontKoreanFallback), true); } } private double _uiFontSize = 13.0; public double UiFontSize { get => _uiFontSize; set { if (Set(ref _uiFontSize, value)) OnSettingChanged(nameof(UiFontSize), true); } } // 2. 일반 & 내비게이션 private string _outputFolder = ""; public string OutputFolder { get => _outputFolder; set { if (Set(ref _outputFolder, value)) OnSettingChanged(nameof(OutputFolder)); } } private string _homeUrl = "https://www.google.com"; public string HomeUrl { get => _homeUrl; set { if (Set(ref _homeUrl, value)) OnSettingChanged(nameof(HomeUrl)); } } private string _language = "ko"; public string Language { get => _language; set { if (Set(ref _language, value)) OnSettingChanged(nameof(Language)); } } private string _searchEngine = "Google"; public string SearchEngine { get => _searchEngine; set { if (Set(ref _searchEngine, value)) OnSettingChanged(nameof(SearchEngine)); } } private bool _autoStart; public bool AutoStart { get => _autoStart; set { if (Set(ref _autoStart, value)) OnSettingChanged(nameof(AutoStart)); } } private bool _minimizeToTray; public bool MinimizeToTray { get => _minimizeToTray; set { if (Set(ref _minimizeToTray, value)) OnSettingChanged(nameof(MinimizeToTray)); } } // 3. 도구 모음 단추 private ObservableCollection _toolbarButtons = new(AppConfig.DefaultToolbarButtons); public ObservableCollection ToolbarButtons { get => _toolbarButtons; set { if (Set(ref _toolbarButtons, value)) OnSettingChanged(nameof(ToolbarButtons), true); } } public bool HasToolbarButton(string key) => _toolbarButtons.Contains(key, StringComparer.OrdinalIgnoreCase); public void ToggleToolbarButton(string key, bool isChecked) { var exists = HasToolbarButton(key); if (isChecked && !exists) { _toolbarButtons.Add(key.ToLowerInvariant()); OnSettingChanged(nameof(ToolbarButtons), true); } else if (!isChecked && exists) { var item = _toolbarButtons.FirstOrDefault(k => string.Equals(k, key, StringComparison.OrdinalIgnoreCase)); if (item != null) { _toolbarButtons.Remove(item); OnSettingChanged(nameof(ToolbarButtons), true); } } } public void SetToolbarButtons(IEnumerable buttons) { _toolbarButtons.Clear(); foreach (var b in buttons) _toolbarButtons.Add(b); OnSettingChanged(nameof(ToolbarButtons), true); } // 4. 다운로드 엔진 private int _concurrency = 8; public int Concurrency { get => _concurrency; set { if (Set(ref _concurrency, value)) OnSettingChanged(nameof(Concurrency)); } } private int _maxConcurrentDownloads = 3; public int MaxConcurrentDownloads { get => _maxConcurrentDownloads; set { if (Set(ref _maxConcurrentDownloads, value)) OnSettingChanged(nameof(MaxConcurrentDownloads)); } } private int _retries = 3; public int Retries { get => _retries; set { if (Set(ref _retries, value)) OnSettingChanged(nameof(Retries)); } } private long _minSizeMB = 0; public long MinSizeMB { get => _minSizeMB; set { if (Set(ref _minSizeMB, value)) OnSettingChanged(nameof(MinSizeMB)); } } private int _maxDownloadSpeedKbps = 0; public int MaxDownloadSpeedKbps { get => _maxDownloadSpeedKbps; set { if (Set(ref _maxDownloadSpeedKbps, value)) OnSettingChanged(nameof(MaxDownloadSpeedKbps)); } } private bool _autoClearCompleted; public bool AutoClearCompleted { get => _autoClearCompleted; set { if (Set(ref _autoClearCompleted, value)) OnSettingChanged(nameof(AutoClearCompleted)); } } private bool _openFolderOnComplete; public bool OpenFolderOnComplete { get => _openFolderOnComplete; set { if (Set(ref _openFolderOnComplete, value)) OnSettingChanged(nameof(OpenFolderOnComplete)); } } private string _subtitleLanguage = ""; public string SubtitleLanguage { get => _subtitleLanguage; set { if (Set(ref _subtitleLanguage, value)) OnSettingChanged(nameof(SubtitleLanguage)); } } private string? _downloadScheduleWindow; public string? DownloadScheduleWindow { get => _downloadScheduleWindow; set { if (Set(ref _downloadScheduleWindow, value)) OnSettingChanged(nameof(DownloadScheduleWindow)); } } // 5. 알림 & 접근성 private bool _notifySound = true; public bool NotifySound { get => _notifySound; set { if (Set(ref _notifySound, value)) OnSettingChanged(nameof(NotifySound)); } } private bool _doNotDisturb; public bool DoNotDisturb { get => _doNotDisturb; set { if (Set(ref _doNotDisturb, value)) OnSettingChanged(nameof(DoNotDisturb)); } } private bool _pauseOnPowerSaver; public bool PauseOnPowerSaver { get => _pauseOnPowerSaver; set { if (Set(ref _pauseOnPowerSaver, value)) OnSettingChanged(nameof(PauseOnPowerSaver)); } } private int _toastDurationMs = 3000; public int ToastDurationMs { get => _toastDurationMs; set { if (Set(ref _toastDurationMs, value)) OnSettingChanged(nameof(ToastDurationMs)); } } private bool _reduceMotion; public bool ReduceMotion { get => _reduceMotion; set { if (Set(ref _reduceMotion, value)) OnSettingChanged(nameof(ReduceMotion), true); } } private bool _showCaptions; public bool ShowCaptions { get => _showCaptions; set { if (Set(ref _showCaptions, value)) OnSettingChanged(nameof(ShowCaptions)); } } private bool _focusSelectsAddress = true; public bool FocusSelectsAddress { get => _focusSelectsAddress; set { if (Set(ref _focusSelectsAddress, value)) OnSettingChanged(nameof(FocusSelectsAddress)); } } private bool _pasteAndGo; public bool PasteAndGo { get => _pasteAndGo; set { if (Set(ref _pasteAndGo, value)) OnSettingChanged(nameof(PasteAndGo)); } } // 6. 브라우저 & 개인정보 private bool _tabWheelSwitch; public bool TabWheelSwitch { get => _tabWheelSwitch; set { if (Set(ref _tabWheelSwitch, value)) OnSettingChanged(nameof(TabWheelSwitch)); } } private string _tabStripPosition = "top"; public string TabStripPosition { get => _tabStripPosition; set { if (Set(ref _tabStripPosition, value)) OnSettingChanged(nameof(TabStripPosition)); } } private bool _verticalTabsDefault; public bool VerticalTabsDefault { get => _verticalTabsDefault; set { if (Set(ref _verticalTabsDefault, value)) OnSettingChanged(nameof(VerticalTabsDefault)); } } private string _verticalTabsPosition = "Left"; public string VerticalTabsPosition { get => _verticalTabsPosition; set { if (Set(ref _verticalTabsPosition, value)) OnSettingChanged(nameof(VerticalTabsPosition)); } } private bool _verticalTabsCollapsed; public bool VerticalTabsCollapsed { get => _verticalTabsCollapsed; set { if (Set(ref _verticalTabsCollapsed, value)) OnSettingChanged(nameof(VerticalTabsCollapsed)); } } private double _verticalTabsWidth = 220.0; public double VerticalTabsWidth { get => _verticalTabsWidth; set { if (Set(ref _verticalTabsWidth, value)) OnSettingChanged(nameof(VerticalTabsWidth)); } } private bool _verticalTabsAutoExpand = true; public bool VerticalTabsAutoExpand { get => _verticalTabsAutoExpand; set { if (Set(ref _verticalTabsAutoExpand, value)) OnSettingChanged(nameof(VerticalTabsAutoExpand)); } } private bool _confirmCloseMultipleTabs = true; public bool ConfirmCloseMultipleTabs { get => _confirmCloseMultipleTabs; set { if (Set(ref _confirmCloseMultipleTabs, value)) OnSettingChanged(nameof(ConfirmCloseMultipleTabs)); } } private bool _bookmarkBarVisible; public bool BookmarkBarVisible { get => _bookmarkBarVisible; set { if (Set(ref _bookmarkBarVisible, value)) OnSettingChanged(nameof(BookmarkBarVisible)); } } private bool _restoreTabsSuspended; public bool RestoreTabsSuspended { get => _restoreTabsSuspended; set { if (Set(ref _restoreTabsSuspended, value)) OnSettingChanged(nameof(RestoreTabsSuspended)); } } private bool _tabTitleMultiline; public bool TabTitleMultiline { get => _tabTitleMultiline; set { if (Set(ref _tabTitleMultiline, value)) OnSettingChanged(nameof(TabTitleMultiline)); } } private List _enabledFormats = new() { "hls", "dash", "mp4", "webm", "mkv", "m4v", "mov", "avi", "audio" }; public List EnabledFormats { get => _enabledFormats; set { if (Set(ref _enabledFormats, value)) OnSettingChanged(nameof(EnabledFormats)); } } // 7. 고급 & 도구 연동 private string? _ffmpegPath; public string? FfmpegPath { get => _ffmpegPath; set { if (Set(ref _ffmpegPath, value)) OnSettingChanged(nameof(FfmpegPath)); } } private string? _ytDlpPath; public string? YtDlpPath { get => _ytDlpPath; set { if (Set(ref _ytDlpPath, value)) OnSettingChanged(nameof(YtDlpPath)); } } private bool _useCookiesForSocial = true; public bool UseCookiesForSocial { get => _useCookiesForSocial; set { if (Set(ref _useCookiesForSocial, value)) OnSettingChanged(nameof(UseCookiesForSocial)); } } private bool _autoInstallYtDlp = true; public bool AutoInstallYtDlp { get => _autoInstallYtDlp; set { if (Set(ref _autoInstallYtDlp, value)) OnSettingChanged(nameof(AutoInstallYtDlp)); } } private bool _autoUpdateYtDlp = true; public bool AutoUpdateYtDlp { get => _autoUpdateYtDlp; set { if (Set(ref _autoUpdateYtDlp, value)) OnSettingChanged(nameof(AutoUpdateYtDlp)); } } private bool _autoInstallDeno = true; public bool AutoInstallDeno { get => _autoInstallDeno; set { if (Set(ref _autoInstallDeno, value)) OnSettingChanged(nameof(AutoInstallDeno)); } } private string _defaultSocialQuality = "best"; public string DefaultSocialQuality { get => _defaultSocialQuality; set { if (Set(ref _defaultSocialQuality, value)) OnSettingChanged(nameof(DefaultSocialQuality)); } } private string? _youtubePlayerClients; public string? YoutubePlayerClients { get => _youtubePlayerClients; set { if (Set(ref _youtubePlayerClients, value)) OnSettingChanged(nameof(YoutubePlayerClients)); } } private string _userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; public string UserAgent { get => _userAgent; set { if (Set(ref _userAgent, value)) OnSettingChanged(nameof(UserAgent)); } } private string _proxyServer = ""; public string ProxyServer { get => _proxyServer; set { if (Set(ref _proxyServer, value)) OnSettingChanged(nameof(ProxyServer)); } } // 8. 업데이트 & 정보 private string _updateRepo = ""; public string UpdateRepo { get => _updateRepo; set { if (Set(ref _updateRepo, value)) OnSettingChanged(nameof(UpdateRepo)); } } // 9. 암호 관리자 & Windows Hello private bool _enableWindowsHelloVault = true; public bool EnableWindowsHelloVault { get => _enableWindowsHelloVault; set { if (Set(ref _enableWindowsHelloVault, value)) OnSettingChanged(nameof(EnableWindowsHelloVault)); } } private int _vaultAutoLockMinutes = 15; public int VaultAutoLockMinutes { get => _vaultAutoLockMinutes; set { if (Set(ref _vaultAutoLockMinutes, value)) OnSettingChanged(nameof(VaultAutoLockMinutes)); } } private bool _requireBiometricForAutofill = false; public bool RequireBiometricForAutofill { get => _requireBiometricForAutofill; set { if (Set(ref _requireBiometricForAutofill, value)) OnSettingChanged(nameof(RequireBiometricForAutofill)); } } private bool _requireBiometricForPasswordReveal = true; public bool RequireBiometricForPasswordReveal { get => _requireBiometricForPasswordReveal; set { if (Set(ref _requireBiometricForPasswordReveal, value)) OnSettingChanged(nameof(RequireBiometricForPasswordReveal)); } } #endregion #region SSOT State Tracking & Validation private bool _isDirty; public bool IsDirty { get => _isDirty; private set => Set(ref _isDirty, value); } private int _dirtyCount; public int DirtyCount { get => _dirtyCount; private set => Set(ref _dirtyCount, value); } private string _validationSummary = ""; public string ValidationSummary { get => _validationSummary; private set => Set(ref _validationSummary, value); } private bool _hasErrors; public bool HasErrors { get => _hasErrors; private set => Set(ref _hasErrors, value); } private void OnSettingChanged(string propertyName, bool requestLivePreview = false) { RecalculateDirtyState(); Validate(); if (requestLivePreview) LivePreviewRequested?.Invoke(propertyName); } public void RecalculateDirtyState() { int changed = 0; if (!Equals(_theme, _originalValues.GetValueOrDefault(nameof(Theme)))) changed++; if (!Equals(_accentColor, _originalValues.GetValueOrDefault(nameof(AccentColor)))) changed++; if (!Equals(_customThemePath, _originalValues.GetValueOrDefault(nameof(CustomThemePath)))) changed++; if (!Equals(_uiDensity, _originalValues.GetValueOrDefault(nameof(UiDensity)))) changed++; if (Math.Abs(_uiScale - (double)(_originalValues.GetValueOrDefault(nameof(UiScale)) ?? 1.0)) > 0.001) changed++; if (Math.Abs(_uiFontScale - (double)(_originalValues.GetValueOrDefault(nameof(UiFontScale)) ?? 1.0)) > 0.001) changed++; if (!Equals(_uiFontFamily, _originalValues.GetValueOrDefault(nameof(UiFontFamily)))) changed++; if (!Equals(_uiFontKoreanFallback, _originalValues.GetValueOrDefault(nameof(UiFontKoreanFallback)))) changed++; if (Math.Abs(_uiFontSize - (double)(_originalValues.GetValueOrDefault(nameof(UiFontSize)) ?? 13.0)) > 0.001) changed++; if (!Equals(_outputFolder, _originalValues.GetValueOrDefault(nameof(OutputFolder)))) changed++; if (!Equals(_homeUrl, _originalValues.GetValueOrDefault(nameof(HomeUrl)))) changed++; if (!Equals(_language, _originalValues.GetValueOrDefault(nameof(Language)))) changed++; if (!Equals(_searchEngine, _originalValues.GetValueOrDefault(nameof(SearchEngine)))) changed++; if (!Equals(_autoStart, _originalValues.GetValueOrDefault(nameof(AutoStart)))) changed++; if (!Equals(_minimizeToTray, _originalValues.GetValueOrDefault(nameof(MinimizeToTray)))) changed++; if (!Equals(_concurrency, _originalValues.GetValueOrDefault(nameof(Concurrency)))) changed++; if (!Equals(_maxConcurrentDownloads, _originalValues.GetValueOrDefault(nameof(MaxConcurrentDownloads)))) changed++; if (!Equals(_retries, _originalValues.GetValueOrDefault(nameof(Retries)))) changed++; if (!Equals(_minSizeMB, _originalValues.GetValueOrDefault(nameof(MinSizeMB)))) changed++; if (!Equals(_maxDownloadSpeedKbps, _originalValues.GetValueOrDefault(nameof(MaxDownloadSpeedKbps)))) changed++; if (!Equals(_autoClearCompleted, _originalValues.GetValueOrDefault(nameof(AutoClearCompleted)))) changed++; if (!Equals(_openFolderOnComplete, _originalValues.GetValueOrDefault(nameof(OpenFolderOnComplete)))) changed++; if (!Equals(_subtitleLanguage, _originalValues.GetValueOrDefault(nameof(SubtitleLanguage)))) changed++; if (!Equals(_downloadScheduleWindow, _originalValues.GetValueOrDefault(nameof(DownloadScheduleWindow)))) changed++; if (!Equals(_notifySound, _originalValues.GetValueOrDefault(nameof(NotifySound)))) changed++; if (!Equals(_doNotDisturb, _originalValues.GetValueOrDefault(nameof(DoNotDisturb)))) changed++; if (!Equals(_pauseOnPowerSaver, _originalValues.GetValueOrDefault(nameof(PauseOnPowerSaver)))) changed++; if (!Equals(_toastDurationMs, _originalValues.GetValueOrDefault(nameof(ToastDurationMs)))) changed++; if (!Equals(_reduceMotion, _originalValues.GetValueOrDefault(nameof(ReduceMotion)))) changed++; if (!Equals(_showCaptions, _originalValues.GetValueOrDefault(nameof(ShowCaptions)))) changed++; if (!Equals(_focusSelectsAddress, _originalValues.GetValueOrDefault(nameof(FocusSelectsAddress)))) changed++; if (!Equals(_pasteAndGo, _originalValues.GetValueOrDefault(nameof(PasteAndGo)))) changed++; if (!Equals(_tabWheelSwitch, _originalValues.GetValueOrDefault(nameof(TabWheelSwitch)))) changed++; if (!Equals(_tabStripPosition, _originalValues.GetValueOrDefault(nameof(TabStripPosition)))) changed++; if (!Equals(_verticalTabsDefault, _originalValues.GetValueOrDefault(nameof(VerticalTabsDefault)))) changed++; if (!Equals(_verticalTabsPosition, _originalValues.GetValueOrDefault(nameof(VerticalTabsPosition)))) changed++; if (!Equals(_verticalTabsCollapsed, _originalValues.GetValueOrDefault(nameof(VerticalTabsCollapsed)))) changed++; if (Math.Abs(_verticalTabsWidth - (double)(_originalValues.GetValueOrDefault(nameof(VerticalTabsWidth)) ?? 220.0)) > 0.001) changed++; if (!Equals(_verticalTabsAutoExpand, _originalValues.GetValueOrDefault(nameof(VerticalTabsAutoExpand)))) changed++; if (!Equals(_confirmCloseMultipleTabs, _originalValues.GetValueOrDefault(nameof(ConfirmCloseMultipleTabs)))) changed++; if (!Equals(_bookmarkBarVisible, _originalValues.GetValueOrDefault(nameof(BookmarkBarVisible)))) changed++; if (!Equals(_restoreTabsSuspended, _originalValues.GetValueOrDefault(nameof(RestoreTabsSuspended)))) changed++; if (!Equals(_tabTitleMultiline, _originalValues.GetValueOrDefault(nameof(TabTitleMultiline)))) changed++; if (!Equals(_ffmpegPath, _originalValues.GetValueOrDefault(nameof(FfmpegPath)))) changed++; if (!Equals(_ytDlpPath, _originalValues.GetValueOrDefault(nameof(YtDlpPath)))) changed++; if (!Equals(_useCookiesForSocial, _originalValues.GetValueOrDefault(nameof(UseCookiesForSocial)))) changed++; if (!Equals(_autoInstallYtDlp, _originalValues.GetValueOrDefault(nameof(AutoInstallYtDlp)))) changed++; if (!Equals(_autoUpdateYtDlp, _originalValues.GetValueOrDefault(nameof(AutoUpdateYtDlp)))) changed++; if (!Equals(_autoInstallDeno, _originalValues.GetValueOrDefault(nameof(AutoInstallDeno)))) changed++; if (!Equals(_defaultSocialQuality, _originalValues.GetValueOrDefault(nameof(DefaultSocialQuality)))) changed++; if (!Equals(_youtubePlayerClients, _originalValues.GetValueOrDefault(nameof(YoutubePlayerClients)))) changed++; if (!Equals(_userAgent, _originalValues.GetValueOrDefault(nameof(UserAgent)))) changed++; if (!Equals(_proxyServer, _originalValues.GetValueOrDefault(nameof(ProxyServer)))) changed++; if (!Equals(_updateRepo, _originalValues.GetValueOrDefault(nameof(UpdateRepo)))) changed++; if (!Equals(_enableWindowsHelloVault, _originalValues.GetValueOrDefault(nameof(EnableWindowsHelloVault)))) changed++; if (!Equals(_vaultAutoLockMinutes, _originalValues.GetValueOrDefault(nameof(VaultAutoLockMinutes)))) changed++; if (!Equals(_requireBiometricForAutofill, _originalValues.GetValueOrDefault(nameof(RequireBiometricForAutofill)))) changed++; if (!Equals(_requireBiometricForPasswordReveal, _originalValues.GetValueOrDefault(nameof(RequireBiometricForPasswordReveal)))) changed++; DirtyCount = changed; IsDirty = changed > 0; } public bool Validate() { _errors.Clear(); if (Concurrency < 1) _errors[nameof(Concurrency)] = "세그먼트 동시 수는 1 이상의 숫자여야 합니다."; if (MaxConcurrentDownloads < 1) _errors[nameof(MaxConcurrentDownloads)] = "최대 동시 다운로드 수는 1 이상의 숫자여야 합니다."; if (Retries < 1) _errors[nameof(Retries)] = "재시도 횟수는 1 이상의 숫자여야 합니다."; if (MinSizeMB < 0) _errors[nameof(MinSizeMB)] = "최소 크기는 0 이상의 숫자여야 합니다."; if (MaxDownloadSpeedKbps < 0) _errors[nameof(MaxDownloadSpeedKbps)] = "속도 상한은 0 이상의 숫자여야 합니다."; if (ToastDurationMs < 500) _errors[nameof(ToastDurationMs)] = "토스트 표시 시간은 0.5초(500ms) 이상이어야 합니다."; if (!string.IsNullOrWhiteSpace(DownloadScheduleWindow) && !Services.DownloadScheduler.TryParse(DownloadScheduleWindow.Trim(), out _, out _)) { _errors[nameof(DownloadScheduleWindow)] = "예약 시간대는 HH:mm-HH:mm 형식이어야 합니다 (예: 02:00-08:00)."; } if (!string.IsNullOrEmpty(AccentColor)) { var acc = AccentColor.Trim(); if (!(acc.Length == 7 && acc.StartsWith('#') && acc[1..].All(Uri.IsHexDigit))) { _errors[nameof(AccentColor)] = "강조색은 #RRGGBB 형식이어야 합니다."; } } HasErrors = _errors.Count > 0; ValidationSummary = HasErrors ? _errors.Values.First() : ""; return !HasErrors; } public void LoadFromConfig(AppConfig config, bool isOriginalSnapshot = false) { _theme = config.Theme ?? "dark"; _accentColor = config.AccentColor; _customThemePath = config.CustomThemePath; _uiDensity = config.UiDensity ?? "normal"; _uiScale = Math.Clamp(config.UiScale, 1.0, 2.0); _uiFontScale = Math.Clamp(config.UiFontScale <= 0 ? 1.0 : config.UiFontScale, 1.0, 2.0); _uiFontFamily = config.UiFontFamily ?? ""; _uiFontKoreanFallback = string.IsNullOrWhiteSpace(config.UiFontKoreanFallback) ? "Malgun Gothic" : config.UiFontKoreanFallback; _uiFontSize = config.UiFontSize > 0 ? config.UiFontSize : 13.0; _outputFolder = config.OutputFolder ?? ""; _homeUrl = string.IsNullOrWhiteSpace(config.HomeUrl) ? "https://www.google.com" : config.HomeUrl; _language = config.Language ?? "ko"; _searchEngine = config.SearchEngine ?? "Google"; _autoStart = config.AutoStart; _minimizeToTray = config.MinimizeToTray; _toolbarButtons = new ObservableCollection(config.ToolbarButtons ?? AppConfig.DefaultToolbarButtons); _concurrency = Math.Max(1, config.Concurrency); _maxConcurrentDownloads = Math.Max(1, config.MaxConcurrentDownloads); _retries = Math.Max(1, config.Retries); _minSizeMB = Math.Max(0, config.MinSizeMB); _maxDownloadSpeedKbps = Math.Max(0, config.MaxDownloadSpeedKbps); _autoClearCompleted = config.AutoClearCompleted; _openFolderOnComplete = config.OpenFolderOnComplete; _subtitleLanguage = config.SubtitleLanguage ?? ""; _downloadScheduleWindow = config.DownloadScheduleWindow; _notifySound = config.NotifySound; _doNotDisturb = config.DoNotDisturb; _pauseOnPowerSaver = config.PauseOnPowerSaver; _toastDurationMs = config.ToastDurationMs > 0 ? config.ToastDurationMs : 3000; _reduceMotion = config.ReduceMotion; _showCaptions = config.ShowCaptions; _focusSelectsAddress = config.FocusSelectsAddress; _pasteAndGo = config.PasteAndGo; _tabWheelSwitch = config.TabWheelSwitch; _tabStripPosition = config.TabStripPosition ?? "top"; _verticalTabsDefault = config.VerticalTabsDefault; _verticalTabsPosition = config.VerticalTabsPosition ?? "Left"; _verticalTabsCollapsed = config.VerticalTabsCollapsed; _verticalTabsWidth = config.VerticalTabsWidth > 0 ? config.VerticalTabsWidth : 220.0; _verticalTabsAutoExpand = config.VerticalTabsAutoExpand; _confirmCloseMultipleTabs = config.ConfirmCloseMultipleTabs; _bookmarkBarVisible = config.BookmarkBarVisible; _restoreTabsSuspended = config.RestoreTabsSuspended; _tabTitleMultiline = config.TabTitleMultiline; _enabledFormats = config.EnabledFormats?.ToList() ?? new List { "hls", "dash", "mp4" }; _ffmpegPath = config.FfmpegPath; _ytDlpPath = config.YtDlpPath; _useCookiesForSocial = config.UseCookiesForSocial; _autoInstallYtDlp = config.AutoInstallYtDlp; _autoUpdateYtDlp = config.AutoUpdateYtDlp; _autoInstallDeno = config.AutoInstallDeno; _defaultSocialQuality = config.DefaultSocialQuality ?? "best"; _youtubePlayerClients = config.YoutubePlayerClients; _userAgent = string.IsNullOrWhiteSpace(config.UserAgent) ? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" : config.UserAgent; _proxyServer = config.ProxyServer ?? ""; _updateRepo = config.UpdateRepo ?? ""; _enableWindowsHelloVault = config.EnableWindowsHelloVault; _vaultAutoLockMinutes = config.VaultAutoLockMinutes; _requireBiometricForAutofill = config.RequireBiometricForAutofill; _requireBiometricForPasswordReveal = config.RequireBiometricForPasswordReveal; if (isOriginalSnapshot) { _originalValues.Clear(); _originalValues[nameof(Theme)] = _theme; _originalValues[nameof(AccentColor)] = _accentColor; _originalValues[nameof(CustomThemePath)] = _customThemePath; _originalValues[nameof(UiDensity)] = _uiDensity; _originalValues[nameof(UiScale)] = _uiScale; _originalValues[nameof(UiFontScale)] = _uiFontScale; _originalValues[nameof(UiFontFamily)] = _uiFontFamily; _originalValues[nameof(UiFontKoreanFallback)] = _uiFontKoreanFallback; _originalValues[nameof(UiFontSize)] = _uiFontSize; _originalValues[nameof(OutputFolder)] = _outputFolder; _originalValues[nameof(HomeUrl)] = _homeUrl; _originalValues[nameof(Language)] = _language; _originalValues[nameof(SearchEngine)] = _searchEngine; _originalValues[nameof(AutoStart)] = _autoStart; _originalValues[nameof(MinimizeToTray)] = _minimizeToTray; _originalValues[nameof(Concurrency)] = _concurrency; _originalValues[nameof(MaxConcurrentDownloads)] = _maxConcurrentDownloads; _originalValues[nameof(Retries)] = _retries; _originalValues[nameof(MinSizeMB)] = _minSizeMB; _originalValues[nameof(MaxDownloadSpeedKbps)] = _maxDownloadSpeedKbps; _originalValues[nameof(AutoClearCompleted)] = _autoClearCompleted; _originalValues[nameof(OpenFolderOnComplete)] = _openFolderOnComplete; _originalValues[nameof(SubtitleLanguage)] = _subtitleLanguage; _originalValues[nameof(DownloadScheduleWindow)] = _downloadScheduleWindow; _originalValues[nameof(NotifySound)] = _notifySound; _originalValues[nameof(DoNotDisturb)] = _doNotDisturb; _originalValues[nameof(PauseOnPowerSaver)] = _pauseOnPowerSaver; _originalValues[nameof(ToastDurationMs)] = _toastDurationMs; _originalValues[nameof(ReduceMotion)] = _reduceMotion; _originalValues[nameof(ShowCaptions)] = _showCaptions; _originalValues[nameof(FocusSelectsAddress)] = _focusSelectsAddress; _originalValues[nameof(PasteAndGo)] = _pasteAndGo; _originalValues[nameof(TabWheelSwitch)] = _tabWheelSwitch; _originalValues[nameof(TabStripPosition)] = _tabStripPosition; _originalValues[nameof(VerticalTabsDefault)] = _verticalTabsDefault; _originalValues[nameof(VerticalTabsPosition)] = _verticalTabsPosition; _originalValues[nameof(VerticalTabsCollapsed)] = _verticalTabsCollapsed; _originalValues[nameof(VerticalTabsWidth)] = _verticalTabsWidth; _originalValues[nameof(VerticalTabsAutoExpand)] = _verticalTabsAutoExpand; _originalValues[nameof(ConfirmCloseMultipleTabs)] = _confirmCloseMultipleTabs; _originalValues[nameof(BookmarkBarVisible)] = _bookmarkBarVisible; _originalValues[nameof(RestoreTabsSuspended)] = _restoreTabsSuspended; _originalValues[nameof(TabTitleMultiline)] = _tabTitleMultiline; _originalValues[nameof(FfmpegPath)] = _ffmpegPath; _originalValues[nameof(YtDlpPath)] = _ytDlpPath; _originalValues[nameof(UseCookiesForSocial)] = _useCookiesForSocial; _originalValues[nameof(AutoInstallYtDlp)] = _autoInstallYtDlp; _originalValues[nameof(AutoUpdateYtDlp)] = _autoUpdateYtDlp; _originalValues[nameof(AutoInstallDeno)] = _autoInstallDeno; _originalValues[nameof(DefaultSocialQuality)] = _defaultSocialQuality; _originalValues[nameof(YoutubePlayerClients)] = _youtubePlayerClients; _originalValues[nameof(UserAgent)] = _userAgent; _originalValues[nameof(ProxyServer)] = _proxyServer; _originalValues[nameof(UpdateRepo)] = _updateRepo; _originalValues[nameof(EnableWindowsHelloVault)] = _enableWindowsHelloVault; _originalValues[nameof(VaultAutoLockMinutes)] = _vaultAutoLockMinutes; _originalValues[nameof(RequireBiometricForAutofill)] = _requireBiometricForAutofill; _originalValues[nameof(RequireBiometricForPasswordReveal)] = _requireBiometricForPasswordReveal; } RecalculateDirtyState(); Validate(); OnPropertyChanged(""); // 전체 프로퍼티 리프레시 } public void CommitToConfig(AppConfig target) { target.Theme = _theme; target.AccentColor = string.IsNullOrWhiteSpace(_accentColor) ? null : _accentColor.Trim(); target.CustomThemePath = string.IsNullOrWhiteSpace(_customThemePath) ? null : _customThemePath.Trim(); target.UiDensity = _uiDensity; target.UiScale = _uiScale; target.UiFontScale = _uiFontScale; target.UiFontFamily = _uiFontFamily.Trim(); target.UiFontKoreanFallback = string.IsNullOrWhiteSpace(_uiFontKoreanFallback) ? "Malgun Gothic" : _uiFontKoreanFallback.Trim(); target.UiFontSize = _uiFontSize; target.OutputFolder = _outputFolder.Trim(); target.HomeUrl = string.IsNullOrWhiteSpace(_homeUrl) ? "https://www.google.com" : _homeUrl.Trim(); target.Language = _language; target.SearchEngine = _searchEngine; target.AutoStart = _autoStart; target.MinimizeToTray = _minimizeToTray; target.ToolbarButtons = _toolbarButtons.ToList(); target.Concurrency = Math.Max(1, _concurrency); target.MaxConcurrentDownloads = Math.Max(1, _maxConcurrentDownloads); target.Retries = Math.Max(1, _retries); target.MinSizeMB = Math.Max(0, _minSizeMB); target.MaxDownloadSpeedKbps = Math.Max(0, _maxDownloadSpeedKbps); target.AutoClearCompleted = _autoClearCompleted; target.OpenFolderOnComplete = _openFolderOnComplete; target.SubtitleLanguage = _subtitleLanguage ?? ""; target.DownloadScheduleWindow = string.IsNullOrWhiteSpace(_downloadScheduleWindow) ? null : _downloadScheduleWindow.Trim(); target.NotifySound = _notifySound; target.DoNotDisturb = _doNotDisturb; target.PauseOnPowerSaver = _pauseOnPowerSaver; target.ToastDurationMs = Math.Max(500, _toastDurationMs); target.ReduceMotion = _reduceMotion; target.ShowCaptions = _showCaptions; target.FocusSelectsAddress = _focusSelectsAddress; target.PasteAndGo = _pasteAndGo; target.TabWheelSwitch = _tabWheelSwitch; target.TabStripPosition = _tabStripPosition; target.VerticalTabsDefault = _verticalTabsDefault; target.VerticalTabsPosition = _verticalTabsPosition; target.VerticalTabsCollapsed = _verticalTabsCollapsed; target.VerticalTabsWidth = _verticalTabsWidth; target.VerticalTabsAutoExpand = _verticalTabsAutoExpand; target.ConfirmCloseMultipleTabs = _confirmCloseMultipleTabs; target.BookmarkBarVisible = _bookmarkBarVisible; target.RestoreTabsSuspended = _restoreTabsSuspended; target.TabTitleMultiline = _tabTitleMultiline; target.EnabledFormats = _enabledFormats.ToList(); target.FfmpegPath = string.IsNullOrWhiteSpace(_ffmpegPath) ? null : _ffmpegPath.Trim(); target.YtDlpPath = string.IsNullOrWhiteSpace(_ytDlpPath) ? null : _ytDlpPath.Trim(); target.UseCookiesForSocial = _useCookiesForSocial; target.AutoInstallYtDlp = _autoInstallYtDlp; target.AutoUpdateYtDlp = _autoUpdateYtDlp; target.AutoInstallDeno = _autoInstallDeno; target.DefaultSocialQuality = _defaultSocialQuality; target.YoutubePlayerClients = string.IsNullOrWhiteSpace(_youtubePlayerClients) ? null : _youtubePlayerClients.Trim(); target.UserAgent = string.IsNullOrWhiteSpace(_userAgent) ? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" : _userAgent.Trim(); target.ProxyServer = _proxyServer.Trim(); target.UpdateRepo = _updateRepo.Trim(); target.EnableWindowsHelloVault = _enableWindowsHelloVault; target.VaultAutoLockMinutes = _vaultAutoLockMinutes; target.RequireBiometricForAutofill = _requireBiometricForAutofill; target.RequireBiometricForPasswordReveal = _requireBiometricForPasswordReveal; } public void ResetToDefaults() { var temp = new AppConfig(); temp.ResetToDefaults(); LoadFromConfig(temp, isOriginalSnapshot: false); } public void ResetToOriginal() { LoadFromConfig(_originalConfig, isOriginalSnapshot: true); } #endregion private bool Set(ref T field, T value, [CallerMemberName] string? name = null) { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; OnPropertyChanged(name); return true; } private void OnPropertyChanged([CallerMemberName] string? name = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } }