forked from sim0n00ps/OF-DL
561 lines
19 KiB
C#
561 lines
19 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();
|
|
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 && !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;
|
|
|
|
private bool _isNormalizingFileNameFormatInput;
|
|
|
|
[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)
|
|
{
|
|
if (IsFileNameFormatField && !_isNormalizingFileNameFormatInput)
|
|
{
|
|
string trimmedValue = value.Trim();
|
|
if (!string.Equals(value, trimmedValue, StringComparison.Ordinal))
|
|
{
|
|
_isNormalizingFileNameFormatInput = true;
|
|
TextValue = trimmedValue;
|
|
_isNormalizingFileNameFormatInput = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|