865 lines
37 KiB
C#
865 lines
37 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Runtime.CompilerServices;
|
|
using Paca.Core.Config;
|
|
|
|
namespace Paca.App.ViewModels;
|
|
|
|
/// <summary>
|
|
/// 설정 화면의 Input SSOT (Single Source of Truth) 뷰모델.
|
|
/// 원본 AppConfig와의 비교를 통한 실시간 Dirty 추적, 유효성 검사, 라이브 프리뷰, 초기화/가져오기/내보내기 일원화 관리.
|
|
/// </summary>
|
|
public sealed class SettingsViewModel : INotifyPropertyChanged
|
|
{
|
|
private readonly AppConfig _originalConfig;
|
|
private readonly Dictionary<string, object?> _originalValues = new(StringComparer.Ordinal);
|
|
private readonly Dictionary<string, string> _errors = new(StringComparer.Ordinal);
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
public event Action<string>? 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<string> _toolbarButtons = new(AppConfig.DefaultToolbarButtons);
|
|
public ObservableCollection<string> 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<string> 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<string> _enabledFormats = new() { "hls", "dash", "mp4", "webm", "mkv", "m4v", "mov", "avi", "audio" };
|
|
public List<string> 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<string>(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<string> { "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<T>(ref T field, T value, [CallerMemberName] string? name = null)
|
|
{
|
|
if (EqualityComparer<T>.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));
|
|
}
|
|
}
|