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));
}
}