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">
-