using System.Collections.ObjectModel; using System.Reflection; using System.Text.RegularExpressions; using Avalonia; using Avalonia.Styling; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Newtonsoft.Json; using OF_DL.Models.Config; namespace OF_DL.Gui.ViewModels; public partial class ConfigFieldViewModel : ViewModelBase { private const string NoListSelectedValue = ""; private const string NoListSelectedDisplayName = "(No list selected)"; private static readonly Regex s_fileNameVariableRegex = new(@"\{([^{}]+)\}", RegexOptions.Compiled); private static readonly Dictionary s_fileNameVariablesByConfigOption = new(StringComparer.Ordinal) { [nameof(Config.PaidPostFileNameFormat)] = [ "id", "postedAt", "mediaId", "mediaCreatedAt", "filename", "username", "text" ], [nameof(Config.PostFileNameFormat)] = [ "id", "postedAt", "mediaId", "mediaCreatedAt", "filename", "username", "text", "rawText" ], [nameof(Config.PaidMessageFileNameFormat)] = [ "id", "createdAt", "mediaId", "mediaCreatedAt", "filename", "username", "text" ], [nameof(Config.MessageFileNameFormat)] = [ "id", "createdAt", "mediaId", "mediaCreatedAt", "filename", "username", "text" ] }; private static readonly Dictionary s_enumDisplayNames = new(StringComparer.Ordinal) { ["_240"] = "240p", ["_720"] = "720p", ["source"] = "Source Resolution", ["light"] = "Light", ["dark"] = "Dark" }; public ConfigFieldViewModel( PropertyInfo propertyInfo, object? initialValue, IEnumerable? ignoredUsersListNames = null, string? helpText = null) { PropertyInfo = propertyInfo; PropertyName = propertyInfo.Name; DisplayName = ToDisplayName(propertyInfo.Name); PropertyType = propertyInfo.PropertyType; HelpText = helpText?.Trim() ?? string.Empty; IsBoolean = PropertyType == typeof(bool); IsEnum = PropertyType.IsEnum; IsDate = PropertyType == typeof(DateTime?); IsNumeric = PropertyType == typeof(int) || PropertyType == typeof(int?); IsMultiline = PropertyType == typeof(Dictionary); IsTextInput = !IsBoolean && !IsEnum && !IsDate && !IsNumeric; if (IsEnum) { foreach (string enumName in Enum.GetNames(PropertyType)) { string displayName = s_enumDisplayNames.TryGetValue(enumName, out string? mappedName) ? mappedName : enumName; EnumOptions.Add(displayName); } } if (IsFileNameFormatField) { foreach (string variableName in GetAllowedFileNameVariables()) { AvailableFileNameVariables.Add(variableName); } SelectedFileNameVariable = AvailableFileNameVariables.FirstOrDefault(); string availableVariables = string.Join(", ", AvailableFileNameVariables.Select(variable => $"{{{variable}}}")); string fileNameHelpText = $"Available variables: {availableVariables}. Include {{mediaId}} or {{filename}} to avoid collisions."; HelpText = string.IsNullOrWhiteSpace(HelpText) ? fileNameHelpText : $"{HelpText} {fileNameHelpText}"; } LoadInitialValue(initialValue); if (IsIgnoredUsersListField) { SetIgnoredUsersListOptions(ignoredUsersListNames ?? []); } if (IsFileNameFormatField) { UpdateFileNameFormatPreview(); } } public PropertyInfo PropertyInfo { get; } public string PropertyName { get; } public string DisplayName { get; } public Type PropertyType { get; } public bool IsBoolean { get; } public bool IsEnum { get; } public bool IsDate { get; } public bool IsNumeric { get; } public bool IsNumericAndNotTimeout => IsNumeric && !IsTimeoutField; public bool IsTextInput { get; } public bool IsMultiline { get; } public bool IsFileNameFormatField => s_fileNameVariablesByConfigOption.ContainsKey(PropertyName); public bool IsCreatorConfigsField => string.Equals(PropertyName, nameof(Config.CreatorConfigs), StringComparison.Ordinal); public bool IsIgnoredUsersListField => string.Equals(PropertyName, nameof(Config.IgnoredUsersListName), StringComparison.Ordinal); public bool IsTimeoutField => string.Equals(PropertyName, nameof(Config.Timeout), StringComparison.Ordinal); public bool IsRegularTextInput => IsTextInput && !IsIgnoredUsersListField && !IsCreatorConfigsField; public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText); public double TextBoxMinHeight => IsMultiline ? 150 : 36; public ObservableCollection EnumOptions { get; } = []; public ObservableCollection AvailableFileNameVariables { get; } = []; public ObservableCollection IgnoredUsersListOptions { get; } = []; public ObservableCollection FileNameFormatSegments { get; } = []; [ObservableProperty] private bool _boolValue; [ObservableProperty] private string? _enumValue; [ObservableProperty] private DateTimeOffset? _dateValue; [ObservableProperty] private decimal? _numericValue; private string _actualTextValue = string.Empty; [ObservableProperty] private string _textValue = string.Empty; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(InsertSelectedFileNameVariableCommand))] private string? _selectedFileNameVariable; private bool IsPathField => string.Equals(PropertyName, nameof(Config.FFmpegPath), StringComparison.Ordinal) || string.Equals(PropertyName, nameof(Config.DownloadPath), StringComparison.Ordinal); [ObservableProperty] private ConfigSelectOptionViewModel? _selectedIgnoredUsersListOption; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasHelpText))] private string _helpText = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasUnknownFileNameVariables))] private string _unknownFileNameVariablesMessage = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))] private string _errorMessage = string.Empty; public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage); public bool HasUnknownFileNameVariables => !string.IsNullOrWhiteSpace(UnknownFileNameVariablesMessage); public bool TryGetTypedValue(out object? value, out string? error) { value = null; error = null; if (IsBoolean) { value = BoolValue; return true; } if (IsEnum) { if (string.IsNullOrWhiteSpace(EnumValue)) { error = $"{DisplayName} is required."; return false; } string actualEnumValue = s_enumDisplayNames .FirstOrDefault(kvp => string.Equals(kvp.Value, EnumValue, StringComparison.Ordinal)) .Key ?? EnumValue; if (!Enum.TryParse(PropertyType, actualEnumValue, true, out object? enumResult)) { error = $"{DisplayName} must be one of: {string.Join(", ", EnumOptions)}."; return false; } value = enumResult; return true; } if (PropertyType == typeof(string)) { // Use actual value for path fields when privacy mode is enabled string textToUse = (Program.HidePrivateInfo && IsPathField) ? _actualTextValue : TextValue; value = textToUse.Trim(); return true; } if (PropertyType == typeof(int)) { if (!NumericValue.HasValue) { error = $"{DisplayName} must be a whole number."; return false; } if (decimal.Truncate(NumericValue.Value) != NumericValue.Value) { error = $"{DisplayName} must be a whole number."; return false; } value = (int)NumericValue.Value; return true; } if (PropertyType == typeof(int?)) { if (!NumericValue.HasValue) { value = null; return true; } if (decimal.Truncate(NumericValue.Value) != NumericValue.Value) { error = $"{DisplayName} must be a whole number."; return false; } value = (int)NumericValue.Value; return true; } if (PropertyType == typeof(DateTime?)) { if (!DateValue.HasValue) { value = null; return true; } value = DateValue.Value.DateTime; return true; } if (PropertyType == typeof(Dictionary)) { if (string.IsNullOrWhiteSpace(TextValue)) { value = new Dictionary(); return true; } try { Dictionary? parsed = JsonConvert.DeserializeObject>(TextValue); value = parsed ?? new Dictionary(); return true; } catch (JsonException) { error = $"{DisplayName} must be valid JSON."; return false; } } error = $"{DisplayName} has an unsupported field type."; return false; } public void ClearError() { ErrorMessage = string.Empty; } public void SetError(string message) { ErrorMessage = message; } public void SetIgnoredUsersListOptions(IEnumerable listNames) { if (!IsIgnoredUsersListField) { return; } string selectedValue = SelectedIgnoredUsersListOption?.Value ?? TextValue.Trim(); ConfigSelectOptionViewModel noSelectionOption = new(NoListSelectedValue, NoListSelectedDisplayName); IgnoredUsersListOptions.Clear(); IgnoredUsersListOptions.Add(noSelectionOption); IEnumerable distinctNames = listNames .Where(listName => !string.IsNullOrWhiteSpace(listName)) .Distinct(StringComparer.Ordinal) .OrderBy(listName => listName, StringComparer.OrdinalIgnoreCase); foreach (string listName in distinctNames) { IgnoredUsersListOptions.Add(new ConfigSelectOptionViewModel(listName, listName)); } ConfigSelectOptionViewModel selectedOption = IgnoredUsersListOptions.FirstOrDefault(option => string.Equals(option.Value, selectedValue, StringComparison.Ordinal)) ?? noSelectionOption; SelectedIgnoredUsersListOption = selectedOption; TextValue = selectedOption.Value; } [RelayCommand(CanExecute = nameof(CanInsertSelectedFileNameVariable))] private void InsertSelectedFileNameVariable() { if (string.IsNullOrWhiteSpace(SelectedFileNameVariable)) { return; } string placeholder = $"{{{SelectedFileNameVariable}}}"; TextValue += placeholder; } partial void OnTextValueChanged(string value) { // Store actual value if not the privacy placeholder if (value != "[Hidden for Privacy]") { _actualTextValue = value; } if (!IsFileNameFormatField) { return; } UpdateFileNameFormatPreview(); } private bool CanInsertSelectedFileNameVariable() => IsFileNameFormatField && !string.IsNullOrWhiteSpace(SelectedFileNameVariable); partial void OnSelectedIgnoredUsersListOptionChanged(ConfigSelectOptionViewModel? value) { if (!IsIgnoredUsersListField) { return; } TextValue = value?.Value ?? NoListSelectedValue; } private void LoadInitialValue(object? initialValue) { if (IsBoolean) { BoolValue = initialValue is bool boolValue && boolValue; return; } if (IsEnum) { string? enumName = initialValue?.ToString(); if (!string.IsNullOrEmpty(enumName)) { string displayName = s_enumDisplayNames.TryGetValue(enumName, out string? mappedName) ? mappedName : enumName; EnumValue = displayName; } else { EnumValue = EnumOptions.FirstOrDefault(); } return; } if (PropertyType == typeof(Dictionary)) { Dictionary creatorConfigs = initialValue as Dictionary ?? new Dictionary(); TextValue = JsonConvert.SerializeObject(creatorConfigs, Formatting.Indented); return; } if (PropertyType == typeof(DateTime?)) { DateTime? date = initialValue is DateTime dt ? dt : null; DateValue = date.HasValue ? new DateTimeOffset(date.Value) : null; return; } if (PropertyType == typeof(int)) { NumericValue = initialValue is int intValue ? intValue : 0; return; } if (PropertyType == typeof(int?)) { NumericValue = initialValue is int nullableIntValue ? nullableIntValue : null; return; } string initialText = initialValue?.ToString() ?? string.Empty; _actualTextValue = initialText; // Show privacy placeholder for path fields if flag is set if (Program.HidePrivateInfo && IsPathField && !string.IsNullOrWhiteSpace(initialText)) { TextValue = "[Hidden for Privacy]"; } else { TextValue = initialText; } } private IEnumerable GetAllowedFileNameVariables() => s_fileNameVariablesByConfigOption.TryGetValue(PropertyName, out string[]? variables) ? variables : []; private void UpdateFileNameFormatPreview() { FileNameFormatSegments.Clear(); UnknownFileNameVariablesMessage = string.Empty; if (string.IsNullOrEmpty(TextValue)) { return; } HashSet allowedVariables = new(GetAllowedFileNameVariables(), StringComparer.OrdinalIgnoreCase); HashSet unknownVariables = new(StringComparer.OrdinalIgnoreCase); (string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor) = GetFileNamePreviewColors(); MatchCollection matches = s_fileNameVariableRegex.Matches(TextValue); int currentIndex = 0; foreach (Match match in matches) { if (match.Index > currentIndex) { string plainText = TextValue[currentIndex..match.Index]; FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(plainText, PlainTextColor)); } string variableName = match.Groups[1].Value; bool isAllowedVariable = allowedVariables.Contains(variableName); FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(match.Value, isAllowedVariable ? AllowedVariableColor : InvalidVariableColor)); if (!isAllowedVariable) { unknownVariables.Add(variableName); } currentIndex = match.Index + match.Length; } if (currentIndex < TextValue.Length) { string trailingText = TextValue[currentIndex..]; FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(trailingText, PlainTextColor)); } if (unknownVariables.Count > 0) { string tokens = string.Join(", ", unknownVariables.Select(variable => $"{{{variable}}}")); UnknownFileNameVariablesMessage = $"Unknown variable(s): {tokens}"; } } private static (string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor) GetFileNamePreviewColors() { bool isDarkTheme = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark; return isDarkTheme ? ("#DCE6F7", "#66A6FF", "#FF8C8C") : ("#1F2A44", "#2E6EEA", "#D84E4E"); } private static string ToDisplayName(string propertyName) { if (string.IsNullOrWhiteSpace(propertyName)) { return propertyName; } return string.Concat(propertyName.Select((character, index) => index > 0 && char.IsUpper(character) && !char.IsUpper(propertyName[index - 1]) ? $" {character}" : character.ToString())); } }