diff --git a/OF DL.Gui/Assets/icon.ico b/OF DL.Gui/Assets/icon.ico new file mode 100644 index 0000000..222e2cc Binary files /dev/null and b/OF DL.Gui/Assets/icon.ico differ diff --git a/OF DL.Gui/OF DL.Gui.csproj b/OF DL.Gui/OF DL.Gui.csproj index 94f25be..0f06086 100644 --- a/OF DL.Gui/OF DL.Gui.csproj +++ b/OF DL.Gui/OF DL.Gui.csproj @@ -7,11 +7,11 @@ enable enable true - Icon\download.ico + Assets\icon.ico - + diff --git a/OF DL.Gui/ViewModels/ConfigCategoryViewModel.cs b/OF DL.Gui/ViewModels/ConfigCategoryViewModel.cs index 15f1db0..8b2ae48 100644 --- a/OF DL.Gui/ViewModels/ConfigCategoryViewModel.cs +++ b/OF DL.Gui/ViewModels/ConfigCategoryViewModel.cs @@ -17,11 +17,33 @@ public sealed class ConfigCategoryViewModel : ViewModelBase CustomDateField = fieldList.FirstOrDefault(field => string.Equals(field.PropertyName, nameof(Config.CustomDate), StringComparison.Ordinal)); + LimitDownloadRateField = fieldList.FirstOrDefault(field => + string.Equals(field.PropertyName, nameof(Config.LimitDownloadRate), StringComparison.Ordinal)); + DownloadLimitInMbPerSecField = fieldList.FirstOrDefault(field => + string.Equals(field.PropertyName, nameof(Config.DownloadLimitInMbPerSec), StringComparison.Ordinal)); + + FolderPerPaidPostField = fieldList.FirstOrDefault(field => + string.Equals(field.PropertyName, nameof(Config.FolderPerPaidPost), StringComparison.Ordinal)); + FolderPerPostField = fieldList.FirstOrDefault(field => + string.Equals(field.PropertyName, nameof(Config.FolderPerPost), StringComparison.Ordinal)); + FolderPerPaidMessageField = fieldList.FirstOrDefault(field => + string.Equals(field.PropertyName, nameof(Config.FolderPerPaidMessage), StringComparison.Ordinal)); + FolderPerMessageField = fieldList.FirstOrDefault(field => + string.Equals(field.PropertyName, nameof(Config.FolderPerMessage), StringComparison.Ordinal)); + IEnumerable visibleFields = IsDownloadBehavior ? fieldList.Where(field => field.PropertyName is not nameof(Config.DownloadOnlySpecificDates) and not nameof(Config.DownloadDateSelection) and not nameof(Config.CustomDate)) - : fieldList; + : IsPerformance + ? fieldList.Where(field => field.PropertyName is not nameof(Config.LimitDownloadRate) + and not nameof(Config.DownloadLimitInMbPerSec)) + : IsFolderStructure + ? fieldList.Where(field => field.PropertyName is not nameof(Config.FolderPerPaidPost) + and not nameof(Config.FolderPerPost) + and not nameof(Config.FolderPerPaidMessage) + and not nameof(Config.FolderPerMessage)) + : fieldList; foreach (ConfigFieldViewModel field in visibleFields) { @@ -34,17 +56,48 @@ public sealed class ConfigCategoryViewModel : ViewModelBase public bool IsDownloadBehavior => string.Equals(CategoryName, "Download Behavior", StringComparison.Ordinal); + public bool IsPerformance => + string.Equals(CategoryName, "Performance", StringComparison.Ordinal); + + public bool IsFolderStructure => + string.Equals(CategoryName, "Folder Structure", StringComparison.Ordinal); + + public bool IsFileNaming => + string.Equals(CategoryName, "File Naming", StringComparison.Ordinal); + public ConfigFieldViewModel? DownloadOnlySpecificDatesField { get; } public ConfigFieldViewModel? DownloadDateSelectionField { get; } public ConfigFieldViewModel? CustomDateField { get; } + public ConfigFieldViewModel? LimitDownloadRateField { get; } + + public ConfigFieldViewModel? DownloadLimitInMbPerSecField { get; } + + public ConfigFieldViewModel? FolderPerPaidPostField { get; } + + public ConfigFieldViewModel? FolderPerPostField { get; } + + public ConfigFieldViewModel? FolderPerPaidMessageField { get; } + + public ConfigFieldViewModel? FolderPerMessageField { get; } + public bool HasSpecificDateFilterFields => DownloadOnlySpecificDatesField != null && DownloadDateSelectionField != null && CustomDateField != null; + public bool HasRateLimitFields => + LimitDownloadRateField != null && + DownloadLimitInMbPerSecField != null; + + public bool HasFolderStructureFields => + FolderPerPaidPostField != null && + FolderPerPostField != null && + FolderPerPaidMessageField != null && + FolderPerMessageField != null; + public string SpecificDateFilterHelpText { get @@ -69,7 +122,59 @@ public sealed class ConfigCategoryViewModel : ViewModelBase } } + public string RateLimitHelpText + { + get + { + List parts = []; + if (!string.IsNullOrWhiteSpace(LimitDownloadRateField?.HelpText)) + { + parts.Add(LimitDownloadRateField.HelpText); + } + + if (!string.IsNullOrWhiteSpace(DownloadLimitInMbPerSecField?.HelpText)) + { + parts.Add(DownloadLimitInMbPerSecField.HelpText); + } + + return string.Join(" ", parts.Distinct(StringComparer.Ordinal)); + } + } + + public string FolderStructureHelpText + { + get + { + List parts = []; + if (!string.IsNullOrWhiteSpace(FolderPerPaidPostField?.HelpText)) + { + parts.Add(FolderPerPaidPostField.HelpText); + } + + if (!string.IsNullOrWhiteSpace(FolderPerPostField?.HelpText)) + { + parts.Add(FolderPerPostField.HelpText); + } + + if (!string.IsNullOrWhiteSpace(FolderPerPaidMessageField?.HelpText)) + { + parts.Add(FolderPerPaidMessageField.HelpText); + } + + if (!string.IsNullOrWhiteSpace(FolderPerMessageField?.HelpText)) + { + parts.Add(FolderPerMessageField.HelpText); + } + + return string.Join(" ", parts.Distinct(StringComparer.Ordinal)); + } + } + public bool HasSpecificDateFilterHelpText => !string.IsNullOrWhiteSpace(SpecificDateFilterHelpText); + public bool HasRateLimitHelpText => !string.IsNullOrWhiteSpace(RateLimitHelpText); + + public bool HasFolderStructureHelpText => !string.IsNullOrWhiteSpace(FolderStructureHelpText); + public ObservableCollection Fields { get; } = []; } diff --git a/OF DL.Gui/ViewModels/ConfigFieldViewModel.cs b/OF DL.Gui/ViewModels/ConfigFieldViewModel.cs index 2014edb..abccf52 100644 --- a/OF DL.Gui/ViewModels/ConfigFieldViewModel.cs +++ b/OF DL.Gui/ViewModels/ConfigFieldViewModel.cs @@ -36,6 +36,14 @@ public partial class ConfigFieldViewModel : ViewModelBase ] }; + private static readonly Dictionary s_enumDisplayNames = + new(StringComparer.Ordinal) + { + ["_240"] = "240p", + ["_720"] = "720p", + ["source"] = "Source Resolution" + }; + public ConfigFieldViewModel( PropertyInfo propertyInfo, object? initialValue, @@ -59,7 +67,10 @@ public partial class ConfigFieldViewModel : ViewModelBase { foreach (string enumName in Enum.GetNames(PropertyType)) { - EnumOptions.Add(enumName); + string displayName = s_enumDisplayNames.TryGetValue(enumName, out string? mappedName) + ? mappedName + : enumName; + EnumOptions.Add(displayName); } } @@ -108,16 +119,24 @@ public partial class ConfigFieldViewModel : ViewModelBase 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 IsRegularTextInput => IsTextInput && !IsIgnoredUsersListField; + public bool IsTimeoutField => + string.Equals(PropertyName, nameof(Config.Timeout), StringComparison.Ordinal); + + public bool IsRegularTextInput => IsTextInput && !IsIgnoredUsersListField && !IsCreatorConfigsField; public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText); @@ -182,7 +201,11 @@ public partial class ConfigFieldViewModel : ViewModelBase return false; } - if (!Enum.TryParse(PropertyType, EnumValue, true, out object? enumResult)) + 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; @@ -359,7 +382,18 @@ public partial class ConfigFieldViewModel : ViewModelBase if (IsEnum) { - EnumValue = initialValue?.ToString() ?? EnumOptions.FirstOrDefault(); + 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; } diff --git a/OF DL.Gui/ViewModels/CreatorConfigEditorViewModel.cs b/OF DL.Gui/ViewModels/CreatorConfigEditorViewModel.cs new file mode 100644 index 0000000..9c6a2f4 --- /dev/null +++ b/OF DL.Gui/ViewModels/CreatorConfigEditorViewModel.cs @@ -0,0 +1,118 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using OF_DL.Models.Config; +using Serilog; + +namespace OF_DL.Gui.ViewModels; + +public partial class CreatorConfigEditorViewModel : ViewModelBase +{ + private CreatorConfigRowViewModel? _editingRow; + + public ObservableCollection Rows { get; } = []; + public ObservableCollection AvailableUsers { get; } + + [ObservableProperty] private CreatorConfigModalViewModel _modalViewModel; + + public CreatorConfigEditorViewModel(IEnumerable availableUsers) + { + AvailableUsers = new ObservableCollection(availableUsers); + ModalViewModel = new CreatorConfigModalViewModel(AvailableUsers, OnModalClose, IsUsernameDuplicate); + } + + public void LoadFromConfig(Dictionary configs) + { + Rows.Clear(); + foreach (KeyValuePair kvp in configs.OrderBy(c => c.Key, StringComparer.OrdinalIgnoreCase)) + { + Rows.Add(new CreatorConfigRowViewModel(kvp.Key, kvp.Value, OnDeleteRow, OnEditRow)); + } + } + + public Dictionary ToDictionary() + { + Dictionary result = new(StringComparer.OrdinalIgnoreCase); + foreach (CreatorConfigRowViewModel row in Rows) + { + result[row.Username] = row.Config; + } + return result; + } + + public void UpdateAvailableUsers(IEnumerable users) + { + AvailableUsers.Clear(); + foreach (string user in users.OrderBy(u => u, StringComparer.OrdinalIgnoreCase)) + { + AvailableUsers.Add(user); + } + } + + [RelayCommand] + private void AddCreator() + { + Log.Information("AddCreator command executed"); + Log.Information("ModalViewModel is null: {IsNull}", ModalViewModel == null); + _editingRow = null; + if (ModalViewModel != null) + { + Log.Information("Calling ModalViewModel.OpenForAdd()"); + ModalViewModel.OpenForAdd(); + Log.Information("After OpenForAdd - Modal IsOpen: {IsOpen}", ModalViewModel.IsOpen); + } + else + { + Log.Error("ModalViewModel is null, cannot open modal"); + } + } + + private void OnDeleteRow(CreatorConfigRowViewModel row) + { + Rows.Remove(row); + } + + private void OnEditRow(CreatorConfigRowViewModel row) + { + _editingRow = row; + ModalViewModel.OpenForEdit(row.Username, row.Config); + } + + private bool IsUsernameDuplicate() + { + string username = ModalViewModel.Username.Trim(); + if (_editingRow != null && string.Equals(_editingRow.Username, username, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return Rows.Any(r => string.Equals(r.Username, username, StringComparison.OrdinalIgnoreCase)); + } + + private void OnModalClose(bool confirmed) + { + if (!confirmed) + { + ModalViewModel.IsOpen = false; + return; + } + + (string Username, CreatorConfig Config)? result = ModalViewModel.GetResult(); + if (result == null) + { + return; + } + + if (_editingRow != null) + { + _editingRow.Username = result.Value.Username; + _editingRow.Config = result.Value.Config; + } + else + { + Rows.Add(new CreatorConfigRowViewModel(result.Value.Username, result.Value.Config, OnDeleteRow, OnEditRow)); + } + + ModalViewModel.IsOpen = false; + } +} diff --git a/OF DL.Gui/ViewModels/CreatorConfigModalViewModel.cs b/OF DL.Gui/ViewModels/CreatorConfigModalViewModel.cs new file mode 100644 index 0000000..a2320a3 --- /dev/null +++ b/OF DL.Gui/ViewModels/CreatorConfigModalViewModel.cs @@ -0,0 +1,292 @@ +using System.Collections.ObjectModel; +using System.Text.RegularExpressions; +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; + + [ObservableProperty] private bool _isOpen; + [ObservableProperty] private bool _isEditMode; + [ObservableProperty] private string _originalUsername = string.Empty; + [ObservableProperty] private string _username = string.Empty; + [ObservableProperty] 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] private string _unknownPaidPostVariablesMessage = string.Empty; + [ObservableProperty] private string _unknownPostVariablesMessage = string.Empty; + [ObservableProperty] private string _unknownPaidMessageVariablesMessage = string.Empty; + [ObservableProperty] private string _unknownMessageVariablesMessage = 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 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; + UsernameError = string.Empty; + PaidPostFileNameFormat = string.Empty; + PostFileNameFormat = string.Empty; + PaidMessageFileNameFormat = string.Empty; + MessageFileNameFormat = string.Empty; + 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; + UsernameError = string.Empty; + PaidPostFileNameFormat = config.PaidPostFileNameFormat ?? string.Empty; + PostFileNameFormat = config.PostFileNameFormat ?? string.Empty; + PaidMessageFileNameFormat = config.PaidMessageFileNameFormat ?? string.Empty; + MessageFileNameFormat = config.MessageFileNameFormat ?? string.Empty; + 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 OnPaidPostFileNameFormatChanged(string value) => UpdatePaidPostPreview(); + partial void OnPostFileNameFormatChanged(string value) => UpdatePostPreview(); + partial void OnPaidMessageFileNameFormatChanged(string value) => UpdatePaidMessagePreview(); + partial void OnMessageFileNameFormatChanged(string value) => UpdateMessagePreview(); + + private bool Validate() + { + UsernameError = string.Empty; + + if (string.IsNullOrWhiteSpace(Username)) + { + UsernameError = "Username is required."; + return false; + } + + string trimmedUsername = Username.Trim(); + if (!IsEditMode || trimmedUsername != OriginalUsername) + { + if (_isUsernameDuplicate()) + { + UsernameError = "A config for this username already exists."; + return false; + } + } + + return true; + } + + 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); + + 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, "#1F2A44")); + } + + string variableName = match.Groups[1].Value; + bool isAllowed = allowedSet.Contains(variableName); + segments.Add(new FileNameFormatSegmentViewModel(match.Value, isAllowed ? "#2E6EEA" : "#D84E4E")); + + if (!isAllowed) + { + unknownVariables.Add(variableName); + } + + currentIndex = match.Index + match.Length; + } + + if (currentIndex < format.Length) + { + string trailingText = format[currentIndex..]; + segments.Add(new FileNameFormatSegmentViewModel(trailingText, "#1F2A44")); + } + + if (unknownVariables.Count > 0) + { + string tokens = string.Join(", ", unknownVariables.Select(v => $"{{{v}}}")); + setUnknownMessage($"Unknown variable(s): {tokens}"); + } + } +} diff --git a/OF DL.Gui/ViewModels/CreatorConfigRowViewModel.cs b/OF DL.Gui/ViewModels/CreatorConfigRowViewModel.cs new file mode 100644 index 0000000..52c02be --- /dev/null +++ b/OF DL.Gui/ViewModels/CreatorConfigRowViewModel.cs @@ -0,0 +1,28 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using OF_DL.Models.Config; + +namespace OF_DL.Gui.ViewModels; + +public partial class CreatorConfigRowViewModel : ViewModelBase +{ + private readonly Action _onDelete; + private readonly Action _onEdit; + + [ObservableProperty] private string _username; + [ObservableProperty] private CreatorConfig _config; + + public CreatorConfigRowViewModel(string username, CreatorConfig config, Action onDelete, Action onEdit) + { + _username = username; + _config = config; + _onDelete = onDelete; + _onEdit = onEdit; + } + + [RelayCommand] + private void Delete() => _onDelete(this); + + [RelayCommand] + private void Edit() => _onEdit(this); +} diff --git a/OF DL.Gui/ViewModels/MainWindowViewModel.cs b/OF DL.Gui/ViewModels/MainWindowViewModel.cs index b3b0bbc..f173dff 100644 --- a/OF DL.Gui/ViewModels/MainWindowViewModel.cs +++ b/OF DL.Gui/ViewModels/MainWindowViewModel.cs @@ -11,6 +11,7 @@ using OF_DL.Models; using OF_DL.Models.Config; using OF_DL.Models.Downloads; using OF_DL.Services; +using Serilog; using UserEntities = OF_DL.Models.Entities.Users; namespace OF_DL.Gui.ViewModels; @@ -148,6 +149,8 @@ public partial class MainWindowViewModel( public ObservableCollection ActivityLog { get; } = []; + [ObservableProperty] private CreatorConfigEditorViewModel _creatorConfigEditor = new(Array.Empty()); + [ObservableProperty] private AppScreen _currentScreen = AppScreen.Loading; [ObservableProperty] private string _statusMessage = "Initializing..."; @@ -162,22 +165,18 @@ public partial class MainWindowViewModel( [ObservableProperty] private string _ffmpegPath = string.Empty; - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasFfmpegPathError))] + [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasFfmpegPathError))] private string _ffmpegPathError = string.Empty; [ObservableProperty] private string _downloadPath = string.Empty; - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasDownloadPathError))] + [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasDownloadPathError))] private string _downloadPathError = string.Empty; - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasMediaTypesError))] + [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasMediaTypesError))] private string _mediaTypesError = string.Empty; - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasMediaSourcesError))] + [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasMediaSourcesError))] private string _mediaSourcesError = string.Empty; [ObservableProperty] private string _authenticatedUserDisplay = "Not authenticated."; @@ -225,6 +224,66 @@ public partial class MainWindowViewModel( public string SelectedUsersSummary => $"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected"; + public bool? AllUsersSelected + { + get + { + if (AvailableUsers.Count == 0) + { + return false; + } + + int selectedCount = AvailableUsers.Count(user => user.IsSelected); + if (selectedCount == 0) + { + return false; + } + + if (selectedCount == AvailableUsers.Count) + { + return true; + } + + return null; + } + set + { + bool? current = AllUsersSelected; + bool shouldSelectAll; + + if (current == true) + { + shouldSelectAll = false; + } + else if (current == false) + { + shouldSelectAll = true; + } + else + { + shouldSelectAll = value == true; + } + + _isUpdatingAllUsersSelected = true; + try + { + foreach (SelectableUserViewModel user in AvailableUsers) + { + user.IsSelected = shouldSelectAll; + } + } + finally + { + _isUpdatingAllUsersSelected = false; + } + + OnPropertyChanged(nameof(SelectedUsersSummary)); + DownloadSelectedCommand.NotifyCanExecuteChanged(); + } + } + + private bool _isUpdatingAllUsersSelected; + public async Task InitializeAsync() { if (HasInitialized) @@ -249,10 +308,7 @@ public partial class MainWindowViewModel( } [RelayCommand] - private async Task RetryStartupAsync() - { - await BeginStartupAsync(); - } + private async Task RetryStartupAsync() => await BeginStartupAsync(); [RelayCommand] private void ExitApplication() @@ -331,10 +387,7 @@ public partial class MainWindowViewModel( } [RelayCommand(CanExecute = nameof(CanRefreshUsers))] - private async Task RefreshUsersAsync() - { - await LoadUsersAndListsAsync(); - } + private async Task RefreshUsersAsync() => await LoadUsersAndListsAsync(); [RelayCommand(CanExecute = nameof(CanRefreshIgnoredUsersLists))] private async Task RefreshIgnoredUsersListsAsync(ConfigFieldViewModel? field) @@ -403,6 +456,27 @@ public partial class MainWindowViewModel( await EnsureAuthenticationAndLoadUsersAsync(); } + [RelayCommand] + private void AddCreatorConfig() + { + Log.Information("=== AddCreatorConfig command called ==="); + Log.Information("CreatorConfigEditor is null: {IsNull}", CreatorConfigEditor == null); + if (CreatorConfigEditor != null) + { + Log.Information("CreatorConfigEditor.AddCreatorCommand is null: {IsNull}", CreatorConfigEditor.AddCreatorCommand == null); + Log.Information("ModalViewModel is null: {IsNull}", CreatorConfigEditor.ModalViewModel == null); + if (CreatorConfigEditor.ModalViewModel != null) + { + Log.Information("ModalViewModel.IsOpen before: {IsOpen}", CreatorConfigEditor.ModalViewModel.IsOpen); + } + CreatorConfigEditor.AddCreatorCommand.Execute(null); + if (CreatorConfigEditor.ModalViewModel != null) + { + Log.Information("ModalViewModel.IsOpen after: {IsOpen}", CreatorConfigEditor.ModalViewModel.IsOpen); + } + } + } + [RelayCommand] private async Task StartBrowserLoginAsync() { @@ -426,6 +500,7 @@ public partial class MainWindowViewModel( return; } + await authService.SaveToFileAsync(); bool isAuthValid = await ValidateCurrentAuthAsync(); if (!isAuthValid) @@ -439,29 +514,6 @@ public partial class MainWindowViewModel( await LoadUsersAndListsAsync(); } - [RelayCommand] - private void SelectAllUsers() - { - foreach (SelectableUserViewModel user in AvailableUsers) - { - user.IsSelected = true; - } - - OnPropertyChanged(nameof(SelectedUsersSummary)); - AppendLog($"Selected all users ({AvailableUsers.Count})."); - } - - [RelayCommand] - private void SelectNoUsers() - { - foreach (SelectableUserViewModel user in AvailableUsers) - { - user.IsSelected = false; - } - - OnPropertyChanged(nameof(SelectedUsersSummary)); - } - [RelayCommand(CanExecute = nameof(CanApplySelectedList))] private async Task SelectUsersFromListAsync() { @@ -492,6 +544,7 @@ public partial class MainWindowViewModel( StatusMessage = $"Selected {selectedUsernames.Count} users from list '{SelectedListName}'."; OnPropertyChanged(nameof(SelectedUsersSummary)); + OnPropertyChanged(nameof(AllUsersSelected)); DownloadSelectedCommand.NotifyCanExecuteChanged(); } finally @@ -502,16 +555,10 @@ public partial class MainWindowViewModel( } [RelayCommand(CanExecute = nameof(CanDownloadSelected))] - private async Task DownloadSelectedAsync() - { - await RunDownloadAsync(downloadPurchasedTabOnly: false); - } + private async Task DownloadSelectedAsync() => await RunDownloadAsync(false); [RelayCommand(CanExecute = nameof(CanDownloadPurchasedTab))] - private async Task DownloadPurchasedTabAsync() - { - await RunDownloadAsync(downloadPurchasedTabOnly: true); - } + private async Task DownloadPurchasedTabAsync() => await RunDownloadAsync(true); [RelayCommand(CanExecute = nameof(CanStopWork))] private void StopWork() @@ -682,15 +729,9 @@ public partial class MainWindowViewModel( _ = SelectUsersFromListAsync(); } - partial void OnFfmpegPathChanged(string value) - { - FfmpegPathError = string.Empty; - } + partial void OnFfmpegPathChanged(string value) => FfmpegPathError = string.Empty; - partial void OnDownloadPathChanged(string value) - { - DownloadPathError = string.Empty; - } + partial void OnDownloadPathChanged(string value) => DownloadPathError = string.Empty; private async Task BeginStartupAsync() { @@ -838,10 +879,13 @@ public partial class MainWindowViewModel( userViewModel.PropertyChanged += OnSelectableUserPropertyChanged; AvailableUsers.Add(userViewModel); } + OnPropertyChanged(nameof(SelectedUsersSummary)); + OnPropertyChanged(nameof(AllUsersSelected)); UpdateUserListsCollection(); UpdateIgnoredUsersListFieldOptions(); + CreatorConfigEditor?.UpdateAvailableUsers(_allUsers.Keys); SelectedListName = null; @@ -896,6 +940,7 @@ public partial class MainWindowViewModel( } ApplySpecialConfigValues(config); + config.CreatorConfigs = CreatorConfigEditor.ToDictionary(); ValidateSpecialConfigValues(); if (HasSpecialConfigErrors()) { @@ -920,13 +965,6 @@ public partial class MainWindowViewModel( if (fieldMap.TryGetValue(error.Key, out ConfigFieldViewModel? field)) { field.SetError(error.Value); - continue; - } - - if (error.Key.StartsWith($"{nameof(Config.CreatorConfigs)}.", StringComparison.Ordinal) && - fieldMap.TryGetValue(nameof(Config.CreatorConfigs), out ConfigFieldViewModel? creatorConfigsField)) - { - creatorConfigsField.SetError(error.Value); } } @@ -941,6 +979,9 @@ public partial class MainWindowViewModel( ConfigCategoriesRight.Clear(); BuildSpecialConfigInputs(config); + CreatorConfigEditor = new CreatorConfigEditorViewModel(_allUsers.Keys); + CreatorConfigEditor.LoadFromConfig(config.CreatorConfigs); + IEnumerable properties = typeof(Config) .GetProperties() .Where(property => property.CanRead && property.CanWrite) @@ -968,7 +1009,11 @@ public partial class MainWindowViewModel( int categoryIndex = 0; foreach (IGrouping group in grouped) { - ConfigCategoryViewModel category = new(group.Key, group.OrderBy(field => field.DisplayName)); + IEnumerable orderedFields = group.Key == "File Naming" + ? group.OrderBy(field => GetFieldOrder(group.Key, field.PropertyName)) + : group.OrderBy(field => field.DisplayName); + + ConfigCategoryViewModel category = new(group.Key, orderedFields); ConfigCategories.Add(category); if (categoryIndex % 2 == 0) { @@ -983,6 +1028,25 @@ public partial class MainWindowViewModel( } } + private static int GetFieldOrder(string categoryName, string propertyName) + { + if (categoryName == "File Naming") + { + return propertyName switch + { + nameof(Config.RenameExistingFilesWhenCustomFormatIsSelected) => 0, + nameof(Config.PaidPostFileNameFormat) => 1, + nameof(Config.PostFileNameFormat) => 2, + nameof(Config.PaidMessageFileNameFormat) => 3, + nameof(Config.MessageFileNameFormat) => 4, + nameof(Config.CreatorConfigs) => 5, + _ => 100 + }; + } + + return 0; + } + private void UpdateIgnoredUsersListFieldOptions() { IEnumerable listNames = _allLists.Keys @@ -1213,12 +1277,16 @@ public partial class MainWindowViewModel( if (e.PropertyName == nameof(SelectableUserViewModel.IsSelected)) { OnPropertyChanged(nameof(SelectedUsersSummary)); + if (!_isUpdatingAllUsersSelected) + { + OnPropertyChanged(nameof(AllUsersSelected)); + } + DownloadSelectedCommand.NotifyCanExecuteChanged(); } } - private void StartDownloadProgress(string description, long maxValue, bool showSize) - { + private void StartDownloadProgress(string description, long maxValue, bool showSize) => Dispatcher.UIThread.Post(() => { DownloadProgressDescription = description; @@ -1227,10 +1295,8 @@ public partial class MainWindowViewModel( IsDownloadProgressIndeterminate = maxValue <= 0; IsDownloadProgressVisible = true; }); - } - private void IncrementDownloadProgress(long increment) - { + private void IncrementDownloadProgress(long increment) => Dispatcher.UIThread.Post(() => { if (IsDownloadProgressIndeterminate) @@ -1240,10 +1306,8 @@ public partial class MainWindowViewModel( DownloadProgressValue = Math.Min(DownloadProgressMaximum, DownloadProgressValue + increment); }); - } - private void UpdateProgressStatus(string message) - { + private void UpdateProgressStatus(string message) => Dispatcher.UIThread.Post(() => { if (IsDownloadProgressVisible) @@ -1251,10 +1315,8 @@ public partial class MainWindowViewModel( DownloadProgressDescription = message; } }); - } - private void StopDownloadProgress() - { + private void StopDownloadProgress() => Dispatcher.UIThread.Post(() => { DownloadProgressDescription = string.Empty; @@ -1263,7 +1325,6 @@ public partial class MainWindowViewModel( IsDownloadProgressIndeterminate = false; IsDownloadProgressVisible = false; }); - } private void ThrowIfStopRequested() { diff --git a/OF DL.Gui/ViewModels/SelectableUserViewModel.cs b/OF DL.Gui/ViewModels/SelectableUserViewModel.cs index 1a62801..fa47a8d 100644 --- a/OF DL.Gui/ViewModels/SelectableUserViewModel.cs +++ b/OF DL.Gui/ViewModels/SelectableUserViewModel.cs @@ -8,5 +8,13 @@ public partial class SelectableUserViewModel(string username, long userId) : Vie public long UserId { get; } = userId; - [ObservableProperty] private bool _isSelected; + public event EventHandler? SelectionChanged; + + [ObservableProperty] + private bool _isSelected; + + partial void OnIsSelectedChanged(bool value) + { + SelectionChanged?.Invoke(this, EventArgs.Empty); + } } diff --git a/OF DL.Gui/Views/MainWindow.axaml b/OF DL.Gui/Views/MainWindow.axaml index 49ff73a..30bdfe8 100644 --- a/OF DL.Gui/Views/MainWindow.axaml +++ b/OF DL.Gui/Views/MainWindow.axaml @@ -7,7 +7,7 @@ x:DataType="vm:MainWindowViewModel" Width="1320" Height="860" - MinWidth="900" + MinWidth="1150" MinHeight="700" Title="OF DL" Background="#EEF3FB" @@ -70,11 +70,6 @@ Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right"> - + + - + - - + + + + +