Config updates

This commit is contained in:
whimsical-c4lic0 2026-02-14 01:34:57 -06:00
parent 5af26156c7
commit 7cccdd58a0
11 changed files with 1435 additions and 346 deletions

BIN
OF DL.Gui/Assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -7,11 +7,11 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>Icon\download.ico</ApplicationIcon> <ApplicationIcon>Assets\icon.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Include="Icon\download.ico"/> <AvaloniaResource Include="Assets\icon.ico"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -17,11 +17,33 @@ public sealed class ConfigCategoryViewModel : ViewModelBase
CustomDateField = fieldList.FirstOrDefault(field => CustomDateField = fieldList.FirstOrDefault(field =>
string.Equals(field.PropertyName, nameof(Config.CustomDate), StringComparison.Ordinal)); 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<ConfigFieldViewModel> visibleFields = IsDownloadBehavior IEnumerable<ConfigFieldViewModel> visibleFields = IsDownloadBehavior
? fieldList.Where(field => field.PropertyName is not nameof(Config.DownloadOnlySpecificDates) ? fieldList.Where(field => field.PropertyName is not nameof(Config.DownloadOnlySpecificDates)
and not nameof(Config.DownloadDateSelection) and not nameof(Config.DownloadDateSelection)
and not nameof(Config.CustomDate)) 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) foreach (ConfigFieldViewModel field in visibleFields)
{ {
@ -34,17 +56,48 @@ public sealed class ConfigCategoryViewModel : ViewModelBase
public bool IsDownloadBehavior => public bool IsDownloadBehavior =>
string.Equals(CategoryName, "Download Behavior", StringComparison.Ordinal); 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? DownloadOnlySpecificDatesField { get; }
public ConfigFieldViewModel? DownloadDateSelectionField { get; } public ConfigFieldViewModel? DownloadDateSelectionField { get; }
public ConfigFieldViewModel? CustomDateField { 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 => public bool HasSpecificDateFilterFields =>
DownloadOnlySpecificDatesField != null && DownloadOnlySpecificDatesField != null &&
DownloadDateSelectionField != null && DownloadDateSelectionField != null &&
CustomDateField != null; CustomDateField != null;
public bool HasRateLimitFields =>
LimitDownloadRateField != null &&
DownloadLimitInMbPerSecField != null;
public bool HasFolderStructureFields =>
FolderPerPaidPostField != null &&
FolderPerPostField != null &&
FolderPerPaidMessageField != null &&
FolderPerMessageField != null;
public string SpecificDateFilterHelpText public string SpecificDateFilterHelpText
{ {
get get
@ -69,7 +122,59 @@ public sealed class ConfigCategoryViewModel : ViewModelBase
} }
} }
public string RateLimitHelpText
{
get
{
List<string> 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<string> 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 HasSpecificDateFilterHelpText => !string.IsNullOrWhiteSpace(SpecificDateFilterHelpText);
public bool HasRateLimitHelpText => !string.IsNullOrWhiteSpace(RateLimitHelpText);
public bool HasFolderStructureHelpText => !string.IsNullOrWhiteSpace(FolderStructureHelpText);
public ObservableCollection<ConfigFieldViewModel> Fields { get; } = []; public ObservableCollection<ConfigFieldViewModel> Fields { get; } = [];
} }

View File

@ -36,6 +36,14 @@ public partial class ConfigFieldViewModel : ViewModelBase
] ]
}; };
private static readonly Dictionary<string, string> s_enumDisplayNames =
new(StringComparer.Ordinal)
{
["_240"] = "240p",
["_720"] = "720p",
["source"] = "Source Resolution"
};
public ConfigFieldViewModel( public ConfigFieldViewModel(
PropertyInfo propertyInfo, PropertyInfo propertyInfo,
object? initialValue, object? initialValue,
@ -59,7 +67,10 @@ public partial class ConfigFieldViewModel : ViewModelBase
{ {
foreach (string enumName in Enum.GetNames(PropertyType)) 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 IsNumeric { get; }
public bool IsNumericAndNotTimeout => IsNumeric && !IsTimeoutField;
public bool IsTextInput { get; } public bool IsTextInput { get; }
public bool IsMultiline { get; } public bool IsMultiline { get; }
public bool IsFileNameFormatField => s_fileNameVariablesByConfigOption.ContainsKey(PropertyName); public bool IsFileNameFormatField => s_fileNameVariablesByConfigOption.ContainsKey(PropertyName);
public bool IsCreatorConfigsField =>
string.Equals(PropertyName, nameof(Config.CreatorConfigs), StringComparison.Ordinal);
public bool IsIgnoredUsersListField => public bool IsIgnoredUsersListField =>
string.Equals(PropertyName, nameof(Config.IgnoredUsersListName), StringComparison.Ordinal); 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); public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText);
@ -182,7 +201,11 @@ public partial class ConfigFieldViewModel : ViewModelBase
return false; 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)}."; error = $"{DisplayName} must be one of: {string.Join(", ", EnumOptions)}.";
return false; return false;
@ -359,7 +382,18 @@ public partial class ConfigFieldViewModel : ViewModelBase
if (IsEnum) 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; return;
} }

View File

@ -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<CreatorConfigRowViewModel> Rows { get; } = [];
public ObservableCollection<string> AvailableUsers { get; }
[ObservableProperty] private CreatorConfigModalViewModel _modalViewModel;
public CreatorConfigEditorViewModel(IEnumerable<string> availableUsers)
{
AvailableUsers = new ObservableCollection<string>(availableUsers);
ModalViewModel = new CreatorConfigModalViewModel(AvailableUsers, OnModalClose, IsUsernameDuplicate);
}
public void LoadFromConfig(Dictionary<string, CreatorConfig> configs)
{
Rows.Clear();
foreach (KeyValuePair<string, CreatorConfig> kvp in configs.OrderBy(c => c.Key, StringComparer.OrdinalIgnoreCase))
{
Rows.Add(new CreatorConfigRowViewModel(kvp.Key, kvp.Value, OnDeleteRow, OnEditRow));
}
}
public Dictionary<string, CreatorConfig> ToDictionary()
{
Dictionary<string, CreatorConfig> result = new(StringComparer.OrdinalIgnoreCase);
foreach (CreatorConfigRowViewModel row in Rows)
{
result[row.Username] = row.Config;
}
return result;
}
public void UpdateAvailableUsers(IEnumerable<string> 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;
}
}

View File

@ -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<bool> _onClose;
private readonly Func<bool> _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<string> AvailableUsers { get; }
public ObservableCollection<string> PaidPostVariables { get; } = [];
public ObservableCollection<string> PostVariables { get; } = [];
public ObservableCollection<string> PaidMessageVariables { get; } = [];
public ObservableCollection<string> MessageVariables { get; } = [];
public ObservableCollection<FileNameFormatSegmentViewModel> PaidPostSegments { get; } = [];
public ObservableCollection<FileNameFormatSegmentViewModel> PostSegments { get; } = [];
public ObservableCollection<FileNameFormatSegmentViewModel> PaidMessageSegments { get; } = [];
public ObservableCollection<FileNameFormatSegmentViewModel> 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<string> availableUsers, Action<bool> onClose, Func<bool> isUsernameDuplicate)
{
AvailableUsers = new ObservableCollection<string>(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<FileNameFormatSegmentViewModel> segments, string[] allowedVariables, Action<string> setUnknownMessage)
{
segments.Clear();
setUnknownMessage(string.Empty);
if (string.IsNullOrEmpty(format))
{
return;
}
HashSet<string> allowedSet = new(allowedVariables, StringComparer.OrdinalIgnoreCase);
HashSet<string> 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}");
}
}
}

View File

@ -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<CreatorConfigRowViewModel> _onDelete;
private readonly Action<CreatorConfigRowViewModel> _onEdit;
[ObservableProperty] private string _username;
[ObservableProperty] private CreatorConfig _config;
public CreatorConfigRowViewModel(string username, CreatorConfig config, Action<CreatorConfigRowViewModel> onDelete, Action<CreatorConfigRowViewModel> onEdit)
{
_username = username;
_config = config;
_onDelete = onDelete;
_onEdit = onEdit;
}
[RelayCommand]
private void Delete() => _onDelete(this);
[RelayCommand]
private void Edit() => _onEdit(this);
}

View File

@ -11,6 +11,7 @@ using OF_DL.Models;
using OF_DL.Models.Config; using OF_DL.Models.Config;
using OF_DL.Models.Downloads; using OF_DL.Models.Downloads;
using OF_DL.Services; using OF_DL.Services;
using Serilog;
using UserEntities = OF_DL.Models.Entities.Users; using UserEntities = OF_DL.Models.Entities.Users;
namespace OF_DL.Gui.ViewModels; namespace OF_DL.Gui.ViewModels;
@ -148,6 +149,8 @@ public partial class MainWindowViewModel(
public ObservableCollection<string> ActivityLog { get; } = []; public ObservableCollection<string> ActivityLog { get; } = [];
[ObservableProperty] private CreatorConfigEditorViewModel _creatorConfigEditor = new(Array.Empty<string>());
[ObservableProperty] private AppScreen _currentScreen = AppScreen.Loading; [ObservableProperty] private AppScreen _currentScreen = AppScreen.Loading;
[ObservableProperty] private string _statusMessage = "Initializing..."; [ObservableProperty] private string _statusMessage = "Initializing...";
@ -162,22 +165,18 @@ public partial class MainWindowViewModel(
[ObservableProperty] private string _ffmpegPath = string.Empty; [ObservableProperty] private string _ffmpegPath = string.Empty;
[ObservableProperty] [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasFfmpegPathError))]
[NotifyPropertyChangedFor(nameof(HasFfmpegPathError))]
private string _ffmpegPathError = string.Empty; private string _ffmpegPathError = string.Empty;
[ObservableProperty] private string _downloadPath = string.Empty; [ObservableProperty] private string _downloadPath = string.Empty;
[ObservableProperty] [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasDownloadPathError))]
[NotifyPropertyChangedFor(nameof(HasDownloadPathError))]
private string _downloadPathError = string.Empty; private string _downloadPathError = string.Empty;
[ObservableProperty] [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasMediaTypesError))]
[NotifyPropertyChangedFor(nameof(HasMediaTypesError))]
private string _mediaTypesError = string.Empty; private string _mediaTypesError = string.Empty;
[ObservableProperty] [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasMediaSourcesError))]
[NotifyPropertyChangedFor(nameof(HasMediaSourcesError))]
private string _mediaSourcesError = string.Empty; private string _mediaSourcesError = string.Empty;
[ObservableProperty] private string _authenticatedUserDisplay = "Not authenticated."; [ObservableProperty] private string _authenticatedUserDisplay = "Not authenticated.";
@ -225,6 +224,66 @@ public partial class MainWindowViewModel(
public string SelectedUsersSummary => public string SelectedUsersSummary =>
$"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected"; $"{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() public async Task InitializeAsync()
{ {
if (HasInitialized) if (HasInitialized)
@ -249,10 +308,7 @@ public partial class MainWindowViewModel(
} }
[RelayCommand] [RelayCommand]
private async Task RetryStartupAsync() private async Task RetryStartupAsync() => await BeginStartupAsync();
{
await BeginStartupAsync();
}
[RelayCommand] [RelayCommand]
private void ExitApplication() private void ExitApplication()
@ -331,10 +387,7 @@ public partial class MainWindowViewModel(
} }
[RelayCommand(CanExecute = nameof(CanRefreshUsers))] [RelayCommand(CanExecute = nameof(CanRefreshUsers))]
private async Task RefreshUsersAsync() private async Task RefreshUsersAsync() => await LoadUsersAndListsAsync();
{
await LoadUsersAndListsAsync();
}
[RelayCommand(CanExecute = nameof(CanRefreshIgnoredUsersLists))] [RelayCommand(CanExecute = nameof(CanRefreshIgnoredUsersLists))]
private async Task RefreshIgnoredUsersListsAsync(ConfigFieldViewModel? field) private async Task RefreshIgnoredUsersListsAsync(ConfigFieldViewModel? field)
@ -403,6 +456,27 @@ public partial class MainWindowViewModel(
await EnsureAuthenticationAndLoadUsersAsync(); 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] [RelayCommand]
private async Task StartBrowserLoginAsync() private async Task StartBrowserLoginAsync()
{ {
@ -426,6 +500,7 @@ public partial class MainWindowViewModel(
return; return;
} }
await authService.SaveToFileAsync(); await authService.SaveToFileAsync();
bool isAuthValid = await ValidateCurrentAuthAsync(); bool isAuthValid = await ValidateCurrentAuthAsync();
if (!isAuthValid) if (!isAuthValid)
@ -439,29 +514,6 @@ public partial class MainWindowViewModel(
await LoadUsersAndListsAsync(); 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))] [RelayCommand(CanExecute = nameof(CanApplySelectedList))]
private async Task SelectUsersFromListAsync() private async Task SelectUsersFromListAsync()
{ {
@ -492,6 +544,7 @@ public partial class MainWindowViewModel(
StatusMessage = $"Selected {selectedUsernames.Count} users from list '{SelectedListName}'."; StatusMessage = $"Selected {selectedUsernames.Count} users from list '{SelectedListName}'.";
OnPropertyChanged(nameof(SelectedUsersSummary)); OnPropertyChanged(nameof(SelectedUsersSummary));
OnPropertyChanged(nameof(AllUsersSelected));
DownloadSelectedCommand.NotifyCanExecuteChanged(); DownloadSelectedCommand.NotifyCanExecuteChanged();
} }
finally finally
@ -502,16 +555,10 @@ public partial class MainWindowViewModel(
} }
[RelayCommand(CanExecute = nameof(CanDownloadSelected))] [RelayCommand(CanExecute = nameof(CanDownloadSelected))]
private async Task DownloadSelectedAsync() private async Task DownloadSelectedAsync() => await RunDownloadAsync(false);
{
await RunDownloadAsync(downloadPurchasedTabOnly: false);
}
[RelayCommand(CanExecute = nameof(CanDownloadPurchasedTab))] [RelayCommand(CanExecute = nameof(CanDownloadPurchasedTab))]
private async Task DownloadPurchasedTabAsync() private async Task DownloadPurchasedTabAsync() => await RunDownloadAsync(true);
{
await RunDownloadAsync(downloadPurchasedTabOnly: true);
}
[RelayCommand(CanExecute = nameof(CanStopWork))] [RelayCommand(CanExecute = nameof(CanStopWork))]
private void StopWork() private void StopWork()
@ -682,15 +729,9 @@ public partial class MainWindowViewModel(
_ = SelectUsersFromListAsync(); _ = SelectUsersFromListAsync();
} }
partial void OnFfmpegPathChanged(string value) partial void OnFfmpegPathChanged(string value) => FfmpegPathError = string.Empty;
{
FfmpegPathError = string.Empty;
}
partial void OnDownloadPathChanged(string value) partial void OnDownloadPathChanged(string value) => DownloadPathError = string.Empty;
{
DownloadPathError = string.Empty;
}
private async Task BeginStartupAsync() private async Task BeginStartupAsync()
{ {
@ -838,10 +879,13 @@ public partial class MainWindowViewModel(
userViewModel.PropertyChanged += OnSelectableUserPropertyChanged; userViewModel.PropertyChanged += OnSelectableUserPropertyChanged;
AvailableUsers.Add(userViewModel); AvailableUsers.Add(userViewModel);
} }
OnPropertyChanged(nameof(SelectedUsersSummary)); OnPropertyChanged(nameof(SelectedUsersSummary));
OnPropertyChanged(nameof(AllUsersSelected));
UpdateUserListsCollection(); UpdateUserListsCollection();
UpdateIgnoredUsersListFieldOptions(); UpdateIgnoredUsersListFieldOptions();
CreatorConfigEditor?.UpdateAvailableUsers(_allUsers.Keys);
SelectedListName = null; SelectedListName = null;
@ -896,6 +940,7 @@ public partial class MainWindowViewModel(
} }
ApplySpecialConfigValues(config); ApplySpecialConfigValues(config);
config.CreatorConfigs = CreatorConfigEditor.ToDictionary();
ValidateSpecialConfigValues(); ValidateSpecialConfigValues();
if (HasSpecialConfigErrors()) if (HasSpecialConfigErrors())
{ {
@ -920,13 +965,6 @@ public partial class MainWindowViewModel(
if (fieldMap.TryGetValue(error.Key, out ConfigFieldViewModel? field)) if (fieldMap.TryGetValue(error.Key, out ConfigFieldViewModel? field))
{ {
field.SetError(error.Value); 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(); ConfigCategoriesRight.Clear();
BuildSpecialConfigInputs(config); BuildSpecialConfigInputs(config);
CreatorConfigEditor = new CreatorConfigEditorViewModel(_allUsers.Keys);
CreatorConfigEditor.LoadFromConfig(config.CreatorConfigs);
IEnumerable<System.Reflection.PropertyInfo> properties = typeof(Config) IEnumerable<System.Reflection.PropertyInfo> properties = typeof(Config)
.GetProperties() .GetProperties()
.Where(property => property.CanRead && property.CanWrite) .Where(property => property.CanRead && property.CanWrite)
@ -968,7 +1009,11 @@ public partial class MainWindowViewModel(
int categoryIndex = 0; int categoryIndex = 0;
foreach (IGrouping<string, ConfigFieldViewModel> group in grouped) foreach (IGrouping<string, ConfigFieldViewModel> group in grouped)
{ {
ConfigCategoryViewModel category = new(group.Key, group.OrderBy(field => field.DisplayName)); IEnumerable<ConfigFieldViewModel> 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); ConfigCategories.Add(category);
if (categoryIndex % 2 == 0) 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() private void UpdateIgnoredUsersListFieldOptions()
{ {
IEnumerable<string> listNames = _allLists.Keys IEnumerable<string> listNames = _allLists.Keys
@ -1213,12 +1277,16 @@ public partial class MainWindowViewModel(
if (e.PropertyName == nameof(SelectableUserViewModel.IsSelected)) if (e.PropertyName == nameof(SelectableUserViewModel.IsSelected))
{ {
OnPropertyChanged(nameof(SelectedUsersSummary)); OnPropertyChanged(nameof(SelectedUsersSummary));
if (!_isUpdatingAllUsersSelected)
{
OnPropertyChanged(nameof(AllUsersSelected));
}
DownloadSelectedCommand.NotifyCanExecuteChanged(); DownloadSelectedCommand.NotifyCanExecuteChanged();
} }
} }
private void StartDownloadProgress(string description, long maxValue, bool showSize) private void StartDownloadProgress(string description, long maxValue, bool showSize) =>
{
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
DownloadProgressDescription = description; DownloadProgressDescription = description;
@ -1227,10 +1295,8 @@ public partial class MainWindowViewModel(
IsDownloadProgressIndeterminate = maxValue <= 0; IsDownloadProgressIndeterminate = maxValue <= 0;
IsDownloadProgressVisible = true; IsDownloadProgressVisible = true;
}); });
}
private void IncrementDownloadProgress(long increment) private void IncrementDownloadProgress(long increment) =>
{
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
if (IsDownloadProgressIndeterminate) if (IsDownloadProgressIndeterminate)
@ -1240,10 +1306,8 @@ public partial class MainWindowViewModel(
DownloadProgressValue = Math.Min(DownloadProgressMaximum, DownloadProgressValue + increment); DownloadProgressValue = Math.Min(DownloadProgressMaximum, DownloadProgressValue + increment);
}); });
}
private void UpdateProgressStatus(string message) private void UpdateProgressStatus(string message) =>
{
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
if (IsDownloadProgressVisible) if (IsDownloadProgressVisible)
@ -1251,10 +1315,8 @@ public partial class MainWindowViewModel(
DownloadProgressDescription = message; DownloadProgressDescription = message;
} }
}); });
}
private void StopDownloadProgress() private void StopDownloadProgress() =>
{
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
DownloadProgressDescription = string.Empty; DownloadProgressDescription = string.Empty;
@ -1263,7 +1325,6 @@ public partial class MainWindowViewModel(
IsDownloadProgressIndeterminate = false; IsDownloadProgressIndeterminate = false;
IsDownloadProgressVisible = false; IsDownloadProgressVisible = false;
}); });
}
private void ThrowIfStopRequested() private void ThrowIfStopRequested()
{ {

View File

@ -8,5 +8,13 @@ public partial class SelectableUserViewModel(string username, long userId) : Vie
public long UserId { get; } = userId; 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);
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using OF_DL.Gui.ViewModels; using OF_DL.Gui.ViewModels;
@ -12,6 +14,7 @@ public partial class MainWindow : Window
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://OF DL.Gui/Assets/icon.ico")));
Opened += OnOpened; Opened += OnOpened;
} }