OF-DL/OF DL.Gui/ViewModels/ConfigFieldViewModel.cs

547 lines
18 KiB
C#

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<string, string[]> 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<string, string> s_enumDisplayNames =
new(StringComparer.Ordinal)
{
["_240"] = "240p",
["_720"] = "720p",
["source"] = "Source Resolution",
["light"] = "Light",
["dark"] = "Dark"
};
private static readonly Dictionary<string, string> s_displayNameOverridesByProperty =
new(StringComparer.Ordinal)
{
[nameof(Config.PostFileNameFormat)] = "Free Post File Name Format",
[nameof(Config.MessageFileNameFormat)] = "Free Message File Name Format",
[nameof(Config.DownloadVideoResolution)] = "Video Resolution",
[nameof(Config.IgnoredUsersListName)] = "Ignored Users List",
[nameof(Config.RenameExistingFilesWhenCustomFormatIsSelected)] =
"Rename Existing Files with Custom Formats",
[nameof(Config.DownloadPath)] = "Download Folder",
[nameof(Config.HideMissingCdmKeysWarning)] = "Hide Missing CDM Keys Warning"
};
public ConfigFieldViewModel(
PropertyInfo propertyInfo,
object? initialValue,
IEnumerable<string>? ignoredUsersListNames = null,
string? helpText = null)
{
PropertyInfo = propertyInfo;
PropertyName = propertyInfo.Name;
DisplayName = GetDisplayName(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<string, CreatorConfig>);
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();
const string fileNameHelpText = "Include {mediaId} or {filename} to avoid filename 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 IsHideMissingCdmKeysWarningField =>
string.Equals(PropertyName, nameof(Config.HideMissingCdmKeysWarning), StringComparison.Ordinal);
public bool IsRegularTextInput =>
IsTextInput && !IsIgnoredUsersListField && !IsCreatorConfigsField && !IsFileNameFormatField;
public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText);
public double TextBoxMinHeight => IsMultiline ? 150 : 36;
public ObservableCollection<string> EnumOptions { get; } = [];
public ObservableCollection<string> AvailableFileNameVariables { get; } = [];
public ObservableCollection<ConfigSelectOptionViewModel> IgnoredUsersListOptions { get; } = [];
public ObservableCollection<FileNameFormatSegmentViewModel> 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<string, CreatorConfig>))
{
if (string.IsNullOrWhiteSpace(TextValue))
{
value = new Dictionary<string, CreatorConfig>();
return true;
}
try
{
Dictionary<string, CreatorConfig>? parsed =
JsonConvert.DeserializeObject<Dictionary<string, CreatorConfig>>(TextValue);
value = parsed ?? new Dictionary<string, CreatorConfig>();
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<string> listNames)
{
if (!IsIgnoredUsersListField)
{
return;
}
string selectedValue = SelectedIgnoredUsersListOption?.Value ?? TextValue.Trim();
ConfigSelectOptionViewModel noSelectionOption =
new(NoListSelectedValue, NoListSelectedDisplayName);
IgnoredUsersListOptions.Clear();
IgnoredUsersListOptions.Add(noSelectionOption);
IEnumerable<string> 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<string, CreatorConfig>))
{
Dictionary<string, CreatorConfig> creatorConfigs =
initialValue as Dictionary<string, CreatorConfig> ?? new Dictionary<string, CreatorConfig>();
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<string> GetAllowedFileNameVariables() =>
s_fileNameVariablesByConfigOption.TryGetValue(PropertyName, out string[]? variables)
? variables
: [];
private void UpdateFileNameFormatPreview()
{
FileNameFormatSegments.Clear();
UnknownFileNameVariablesMessage = string.Empty;
if (string.IsNullOrEmpty(TextValue))
{
return;
}
HashSet<string> allowedVariables = new(GetAllowedFileNameVariables(), StringComparer.OrdinalIgnoreCase);
HashSet<string> 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()));
}
private static string GetDisplayName(string propertyName) =>
s_displayNameOverridesByProperty.TryGetValue(propertyName, out string? displayName)
? displayName
: ToDisplayName(propertyName);
}