using System.Collections.ObjectModel; using System.Text.RegularExpressions; using Avalonia; using Avalonia.Styling; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using OF_DL.Models.Config; using Serilog; namespace OF_DL.Gui.ViewModels; public partial class CreatorConfigModalViewModel : ViewModelBase { private static readonly Regex s_fileNameVariableRegex = new(@"\{([^{}]+)\}", RegexOptions.Compiled); private static readonly string[] s_postFileNameVariables = ["id", "postedAt", "mediaId", "mediaCreatedAt", "filename", "username", "text"]; private static readonly string[] s_messageFileNameVariables = ["id", "createdAt", "mediaId", "mediaCreatedAt", "filename", "username", "text"]; private readonly Action _onClose; private readonly Func _isUsernameDuplicate; private bool _isNormalizingUsername; private bool _isNormalizingFileNameFormat; [ObservableProperty] private bool _isOpen; [ObservableProperty] private bool _isEditMode; [ObservableProperty] private string _originalUsername = string.Empty; [ObservableProperty] private string _username = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasUsernameError))] private string _usernameError = string.Empty; [ObservableProperty] private string _paidPostFileNameFormat = string.Empty; [ObservableProperty] private string _postFileNameFormat = string.Empty; [ObservableProperty] private string _paidMessageFileNameFormat = string.Empty; [ObservableProperty] private string _messageFileNameFormat = string.Empty; [ObservableProperty] private string _selectedPaidPostVariable = string.Empty; [ObservableProperty] private string _selectedPostVariable = string.Empty; [ObservableProperty] private string _selectedPaidMessageVariable = string.Empty; [ObservableProperty] private string _selectedMessageVariable = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasUnknownPaidPostVariables))] private string _unknownPaidPostVariablesMessage = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasUnknownPostVariables))] private string _unknownPostVariablesMessage = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasUnknownPaidMessageVariables))] private string _unknownPaidMessageVariablesMessage = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasUnknownMessageVariables))] private string _unknownMessageVariablesMessage = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasPaidPostFileNameFormatError))] private string _paidPostFileNameFormatError = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasPostFileNameFormatError))] private string _postFileNameFormatError = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasPaidMessageFileNameFormatError))] private string _paidMessageFileNameFormatError = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasMessageFileNameFormatError))] private string _messageFileNameFormatError = string.Empty; public ObservableCollection AvailableUsers { get; } public ObservableCollection PaidPostVariables { get; } = []; public ObservableCollection PostVariables { get; } = []; public ObservableCollection PaidMessageVariables { get; } = []; public ObservableCollection MessageVariables { get; } = []; public ObservableCollection PaidPostSegments { get; } = []; public ObservableCollection PostSegments { get; } = []; public ObservableCollection PaidMessageSegments { get; } = []; public ObservableCollection MessageSegments { get; } = []; public bool HasUsernameError => !string.IsNullOrWhiteSpace(UsernameError); public bool HasUnknownPaidPostVariables => !string.IsNullOrWhiteSpace(UnknownPaidPostVariablesMessage); public bool HasUnknownPostVariables => !string.IsNullOrWhiteSpace(UnknownPostVariablesMessage); public bool HasUnknownPaidMessageVariables => !string.IsNullOrWhiteSpace(UnknownPaidMessageVariablesMessage); public bool HasUnknownMessageVariables => !string.IsNullOrWhiteSpace(UnknownMessageVariablesMessage); public bool HasPaidPostFileNameFormatError => !string.IsNullOrWhiteSpace(PaidPostFileNameFormatError); public bool HasPostFileNameFormatError => !string.IsNullOrWhiteSpace(PostFileNameFormatError); public bool HasPaidMessageFileNameFormatError => !string.IsNullOrWhiteSpace(PaidMessageFileNameFormatError); public bool HasMessageFileNameFormatError => !string.IsNullOrWhiteSpace(MessageFileNameFormatError); public string DialogTitle => IsEditMode ? "Edit Creator Config" : "Add Creator Config"; public CreatorConfigModalViewModel(IEnumerable availableUsers, Action onClose, Func isUsernameDuplicate) { AvailableUsers = new ObservableCollection(availableUsers); _onClose = onClose; _isUsernameDuplicate = isUsernameDuplicate; foreach (string variable in s_postFileNameVariables) { PaidPostVariables.Add(variable); PostVariables.Add(variable); } foreach (string variable in s_messageFileNameVariables) { PaidMessageVariables.Add(variable); MessageVariables.Add(variable); } SelectedPaidPostVariable = PaidPostVariables.FirstOrDefault() ?? string.Empty; SelectedPostVariable = PostVariables.FirstOrDefault() ?? string.Empty; SelectedPaidMessageVariable = PaidMessageVariables.FirstOrDefault() ?? string.Empty; SelectedMessageVariable = MessageVariables.FirstOrDefault() ?? string.Empty; } public void OpenForAdd() { Log.Information("=== OpenForAdd called ==="); IsEditMode = false; OriginalUsername = string.Empty; Username = string.Empty; PaidPostFileNameFormat = string.Empty; PostFileNameFormat = string.Empty; PaidMessageFileNameFormat = string.Empty; MessageFileNameFormat = string.Empty; ClearValidationErrors(); ClearAllPreviews(); Log.Information("About to set IsOpen = true"); IsOpen = true; Log.Information("=== OpenForAdd: IsOpen is now {IsOpen} ===", IsOpen); } public void OpenForEdit(string username, CreatorConfig config) { IsEditMode = true; OriginalUsername = username; Username = username; PaidPostFileNameFormat = config.PaidPostFileNameFormat ?? string.Empty; PostFileNameFormat = config.PostFileNameFormat ?? string.Empty; PaidMessageFileNameFormat = config.PaidMessageFileNameFormat ?? string.Empty; MessageFileNameFormat = config.MessageFileNameFormat ?? string.Empty; ClearValidationErrors(); UpdateAllPreviews(); IsOpen = true; } public (string Username, CreatorConfig Config)? GetResult() { if (!Validate()) { return null; } CreatorConfig config = new() { PaidPostFileNameFormat = string.IsNullOrWhiteSpace(PaidPostFileNameFormat) ? null : PaidPostFileNameFormat, PostFileNameFormat = string.IsNullOrWhiteSpace(PostFileNameFormat) ? null : PostFileNameFormat, PaidMessageFileNameFormat = string.IsNullOrWhiteSpace(PaidMessageFileNameFormat) ? null : PaidMessageFileNameFormat, MessageFileNameFormat = string.IsNullOrWhiteSpace(MessageFileNameFormat) ? null : MessageFileNameFormat }; return (Username.Trim(), config); } [RelayCommand] private void InsertPaidPostVariable() { if (!string.IsNullOrWhiteSpace(SelectedPaidPostVariable)) { PaidPostFileNameFormat += $"{{{SelectedPaidPostVariable}}}"; } } [RelayCommand] private void InsertPostVariable() { if (!string.IsNullOrWhiteSpace(SelectedPostVariable)) { PostFileNameFormat += $"{{{SelectedPostVariable}}}"; } } [RelayCommand] private void InsertPaidMessageVariable() { if (!string.IsNullOrWhiteSpace(SelectedPaidMessageVariable)) { PaidMessageFileNameFormat += $"{{{SelectedPaidMessageVariable}}}"; } } [RelayCommand] private void InsertMessageVariable() { if (!string.IsNullOrWhiteSpace(SelectedMessageVariable)) { MessageFileNameFormat += $"{{{SelectedMessageVariable}}}"; } } [RelayCommand] private void Confirm() { if (Validate()) { _onClose(true); } } [RelayCommand] private void Cancel() { _onClose(false); } partial void OnIsOpenChanged(bool value) { Log.Information("*** IsOpen property changed to: {Value} ***", value); } partial void OnUsernameChanged(string value) { if (_isNormalizingUsername) { return; } string trimmed = value.Trim(); if (!string.Equals(value, trimmed, StringComparison.Ordinal)) { _isNormalizingUsername = true; Username = trimmed; _isNormalizingUsername = false; } UsernameError = string.Empty; } partial void OnPaidPostFileNameFormatChanged(string value) => NormalizeFileNameFormat( value, trimmed => PaidPostFileNameFormat = trimmed, () => PaidPostFileNameFormatError = string.Empty, UpdatePaidPostPreview); partial void OnPostFileNameFormatChanged(string value) => NormalizeFileNameFormat( value, trimmed => PostFileNameFormat = trimmed, () => PostFileNameFormatError = string.Empty, UpdatePostPreview); partial void OnPaidMessageFileNameFormatChanged(string value) => NormalizeFileNameFormat( value, trimmed => PaidMessageFileNameFormat = trimmed, () => PaidMessageFileNameFormatError = string.Empty, UpdatePaidMessagePreview); partial void OnMessageFileNameFormatChanged(string value) => NormalizeFileNameFormat( value, trimmed => MessageFileNameFormat = trimmed, () => MessageFileNameFormatError = string.Empty, UpdateMessagePreview); private bool Validate() { ClearValidationErrors(); TrimInputValues(); bool isValid = true; if (string.IsNullOrWhiteSpace(Username)) { UsernameError = "Username is required."; isValid = false; } string trimmedUsername = Username.Trim(); if (isValid && (!IsEditMode || trimmedUsername != OriginalUsername)) { if (_isUsernameDuplicate()) { UsernameError = "A config for this username already exists."; isValid = false; } } ValidateFileNameFormatUniqueness(PaidPostFileNameFormat, message => PaidPostFileNameFormatError = message, ref isValid); ValidateFileNameFormatUniqueness(PostFileNameFormat, message => PostFileNameFormatError = message, ref isValid); ValidateFileNameFormatUniqueness(PaidMessageFileNameFormat, message => PaidMessageFileNameFormatError = message, ref isValid); ValidateFileNameFormatUniqueness(MessageFileNameFormat, message => MessageFileNameFormatError = message, ref isValid); return isValid; } private void ClearValidationErrors() { UsernameError = string.Empty; PaidPostFileNameFormatError = string.Empty; PostFileNameFormatError = string.Empty; PaidMessageFileNameFormatError = string.Empty; MessageFileNameFormatError = string.Empty; } private void TrimInputValues() { Username = Username.Trim(); PaidPostFileNameFormat = PaidPostFileNameFormat.Trim(); PostFileNameFormat = PostFileNameFormat.Trim(); PaidMessageFileNameFormat = PaidMessageFileNameFormat.Trim(); MessageFileNameFormat = MessageFileNameFormat.Trim(); } private static void ValidateFileNameFormatUniqueness( string format, Action setError, ref bool isValid) { if (string.IsNullOrWhiteSpace(format)) { setError(string.Empty); return; } bool hasUniqueToken = format.Contains("{mediaId}", StringComparison.OrdinalIgnoreCase) || format.Contains("{filename}", StringComparison.OrdinalIgnoreCase); if (hasUniqueToken) { setError(string.Empty); return; } setError("Format must include {mediaId} or {filename} to avoid file collisions."); isValid = false; } private void NormalizeFileNameFormat( string value, Action setValue, Action clearError, Action updatePreview) { if (!_isNormalizingFileNameFormat) { string trimmedValue = value.Trim(); if (!string.Equals(value, trimmedValue, StringComparison.Ordinal)) { _isNormalizingFileNameFormat = true; setValue(trimmedValue); _isNormalizingFileNameFormat = false; return; } } clearError(); updatePreview(); } private void ClearAllPreviews() { PaidPostSegments.Clear(); PostSegments.Clear(); PaidMessageSegments.Clear(); MessageSegments.Clear(); UnknownPaidPostVariablesMessage = string.Empty; UnknownPostVariablesMessage = string.Empty; UnknownPaidMessageVariablesMessage = string.Empty; UnknownMessageVariablesMessage = string.Empty; } private void UpdateAllPreviews() { UpdatePaidPostPreview(); UpdatePostPreview(); UpdatePaidMessagePreview(); UpdateMessagePreview(); } private void UpdatePaidPostPreview() => UpdateFileNamePreview(PaidPostFileNameFormat, PaidPostSegments, s_postFileNameVariables, msg => UnknownPaidPostVariablesMessage = msg); private void UpdatePostPreview() => UpdateFileNamePreview(PostFileNameFormat, PostSegments, s_postFileNameVariables, msg => UnknownPostVariablesMessage = msg); private void UpdatePaidMessagePreview() => UpdateFileNamePreview(PaidMessageFileNameFormat, PaidMessageSegments, s_messageFileNameVariables, msg => UnknownPaidMessageVariablesMessage = msg); private void UpdateMessagePreview() => UpdateFileNamePreview(MessageFileNameFormat, MessageSegments, s_messageFileNameVariables, msg => UnknownMessageVariablesMessage = msg); private void UpdateFileNamePreview(string format, ObservableCollection segments, string[] allowedVariables, Action setUnknownMessage) { segments.Clear(); setUnknownMessage(string.Empty); if (string.IsNullOrEmpty(format)) { return; } HashSet allowedSet = new(allowedVariables, StringComparer.OrdinalIgnoreCase); HashSet unknownVariables = new(StringComparer.OrdinalIgnoreCase); (string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor) = GetFileNamePreviewColors(); MatchCollection matches = s_fileNameVariableRegex.Matches(format); int currentIndex = 0; foreach (Match match in matches) { if (match.Index > currentIndex) { string plainText = format[currentIndex..match.Index]; segments.Add(new FileNameFormatSegmentViewModel(plainText, PlainTextColor)); } string variableName = match.Groups[1].Value; bool isAllowed = allowedSet.Contains(variableName); segments.Add(new FileNameFormatSegmentViewModel(match.Value, isAllowed ? AllowedVariableColor : InvalidVariableColor)); if (!isAllowed) { unknownVariables.Add(variableName); } currentIndex = match.Index + match.Length; } if (currentIndex < format.Length) { string trailingText = format[currentIndex..]; segments.Add(new FileNameFormatSegmentViewModel(trailingText, PlainTextColor)); } if (unknownVariables.Count > 0) { string tokens = string.Join(", ", unknownVariables.Select(v => $"{{{v}}}")); setUnknownMessage($"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"); } }