UI improvements to the configuration page
This commit is contained in:
parent
ec8bf47de5
commit
712f11dc4b
@ -96,6 +96,13 @@ public class DownloadOrchestrationService(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the user's lists only.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A dictionary of list names to list IDs.</returns>
|
||||||
|
public async Task<Dictionary<string, long>> GetUserListsAsync() =>
|
||||||
|
await apiService.GetLists("/lists") ?? new Dictionary<string, long>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the users that belong to a specific list.
|
/// Resolves the users that belong to a specific list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -9,6 +9,11 @@ public interface IDownloadOrchestrationService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<UserListResult> GetAvailableUsersAsync();
|
Task<UserListResult> GetAvailableUsersAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetch only user lists.
|
||||||
|
/// </summary>
|
||||||
|
Task<Dictionary<string, long>> GetUserListsAsync();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get users for a specific list by name.
|
/// Get users for a specific list by name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using OF_DL.Models.Config;
|
||||||
|
|
||||||
namespace OF_DL.Gui.ViewModels;
|
namespace OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
@ -7,7 +8,22 @@ public sealed class ConfigCategoryViewModel : ViewModelBase
|
|||||||
public ConfigCategoryViewModel(string categoryName, IEnumerable<ConfigFieldViewModel> fields)
|
public ConfigCategoryViewModel(string categoryName, IEnumerable<ConfigFieldViewModel> fields)
|
||||||
{
|
{
|
||||||
CategoryName = categoryName;
|
CategoryName = categoryName;
|
||||||
foreach (ConfigFieldViewModel field in fields)
|
List<ConfigFieldViewModel> fieldList = fields.ToList();
|
||||||
|
|
||||||
|
DownloadOnlySpecificDatesField = fieldList.FirstOrDefault(field =>
|
||||||
|
string.Equals(field.PropertyName, nameof(Config.DownloadOnlySpecificDates), StringComparison.Ordinal));
|
||||||
|
DownloadDateSelectionField = fieldList.FirstOrDefault(field =>
|
||||||
|
string.Equals(field.PropertyName, nameof(Config.DownloadDateSelection), StringComparison.Ordinal));
|
||||||
|
CustomDateField = fieldList.FirstOrDefault(field =>
|
||||||
|
string.Equals(field.PropertyName, nameof(Config.CustomDate), StringComparison.Ordinal));
|
||||||
|
|
||||||
|
IEnumerable<ConfigFieldViewModel> visibleFields = IsDownloadBehavior
|
||||||
|
? fieldList.Where(field => field.PropertyName is not nameof(Config.DownloadOnlySpecificDates)
|
||||||
|
and not nameof(Config.DownloadDateSelection)
|
||||||
|
and not nameof(Config.CustomDate))
|
||||||
|
: fieldList;
|
||||||
|
|
||||||
|
foreach (ConfigFieldViewModel field in visibleFields)
|
||||||
{
|
{
|
||||||
Fields.Add(field);
|
Fields.Add(field);
|
||||||
}
|
}
|
||||||
@ -18,5 +34,42 @@ 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 ConfigFieldViewModel? DownloadOnlySpecificDatesField { get; }
|
||||||
|
|
||||||
|
public ConfigFieldViewModel? DownloadDateSelectionField { get; }
|
||||||
|
|
||||||
|
public ConfigFieldViewModel? CustomDateField { get; }
|
||||||
|
|
||||||
|
public bool HasSpecificDateFilterFields =>
|
||||||
|
DownloadOnlySpecificDatesField != null &&
|
||||||
|
DownloadDateSelectionField != null &&
|
||||||
|
CustomDateField != null;
|
||||||
|
|
||||||
|
public string SpecificDateFilterHelpText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
List<string> parts = [];
|
||||||
|
if (!string.IsNullOrWhiteSpace(DownloadOnlySpecificDatesField?.HelpText))
|
||||||
|
{
|
||||||
|
parts.Add(DownloadOnlySpecificDatesField.HelpText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(DownloadDateSelectionField?.HelpText))
|
||||||
|
{
|
||||||
|
parts.Add(DownloadDateSelectionField.HelpText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(CustomDateField?.HelpText))
|
||||||
|
{
|
||||||
|
parts.Add(CustomDateField.HelpText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", parts.Distinct(StringComparer.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasSpecificDateFilterHelpText => !string.IsNullOrWhiteSpace(SpecificDateFilterHelpText);
|
||||||
|
|
||||||
public ObservableCollection<ConfigFieldViewModel> Fields { get; } = [];
|
public ObservableCollection<ConfigFieldViewModel> Fields { get; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using OF_DL.Models.Config;
|
using OF_DL.Models.Config;
|
||||||
|
|
||||||
@ -8,12 +10,43 @@ namespace OF_DL.Gui.ViewModels;
|
|||||||
|
|
||||||
public partial class ConfigFieldViewModel : ViewModelBase
|
public partial class ConfigFieldViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public ConfigFieldViewModel(PropertyInfo propertyInfo, object? initialValue)
|
private const string NoListSelectedValue = "";
|
||||||
|
private const string NoListSelectedDisplayName = "(No list selected)";
|
||||||
|
|
||||||
|
private static readonly Regex s_fileNameVariableRegex = new(@"\{([^{}]+)\}", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string[]> s_fileNameVariablesByConfigOption =
|
||||||
|
new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
[nameof(Config.PaidPostFileNameFormat)] =
|
||||||
|
[
|
||||||
|
"id", "postedAt", "mediaId", "mediaCreatedAt", "filename", "username", "text"
|
||||||
|
],
|
||||||
|
[nameof(Config.PostFileNameFormat)] =
|
||||||
|
[
|
||||||
|
"id", "postedAt", "mediaId", "mediaCreatedAt", "filename", "username", "text", "rawText"
|
||||||
|
],
|
||||||
|
[nameof(Config.PaidMessageFileNameFormat)] =
|
||||||
|
[
|
||||||
|
"id", "createdAt", "mediaId", "mediaCreatedAt", "filename", "username", "text"
|
||||||
|
],
|
||||||
|
[nameof(Config.MessageFileNameFormat)] =
|
||||||
|
[
|
||||||
|
"id", "createdAt", "mediaId", "mediaCreatedAt", "filename", "username", "text"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
public ConfigFieldViewModel(
|
||||||
|
PropertyInfo propertyInfo,
|
||||||
|
object? initialValue,
|
||||||
|
IEnumerable<string>? ignoredUsersListNames = null,
|
||||||
|
string? helpText = null)
|
||||||
{
|
{
|
||||||
PropertyInfo = propertyInfo;
|
PropertyInfo = propertyInfo;
|
||||||
PropertyName = propertyInfo.Name;
|
PropertyName = propertyInfo.Name;
|
||||||
DisplayName = ToDisplayName(propertyInfo.Name);
|
DisplayName = ToDisplayName(propertyInfo.Name);
|
||||||
PropertyType = propertyInfo.PropertyType;
|
PropertyType = propertyInfo.PropertyType;
|
||||||
|
HelpText = helpText?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
IsBoolean = PropertyType == typeof(bool);
|
IsBoolean = PropertyType == typeof(bool);
|
||||||
IsEnum = PropertyType.IsEnum;
|
IsEnum = PropertyType.IsEnum;
|
||||||
@ -30,7 +63,33 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsFileNameFormatField)
|
||||||
|
{
|
||||||
|
foreach (string variableName in GetAllowedFileNameVariables())
|
||||||
|
{
|
||||||
|
AvailableFileNameVariables.Add(variableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedFileNameVariable = AvailableFileNameVariables.FirstOrDefault();
|
||||||
|
string availableVariables = string.Join(", ", AvailableFileNameVariables.Select(variable => $"{{{variable}}}"));
|
||||||
|
string fileNameHelpText =
|
||||||
|
$"Available variables: {availableVariables}. Include {{mediaId}} or {{filename}} to avoid collisions.";
|
||||||
|
HelpText = string.IsNullOrWhiteSpace(HelpText)
|
||||||
|
? fileNameHelpText
|
||||||
|
: $"{HelpText} {fileNameHelpText}";
|
||||||
|
}
|
||||||
|
|
||||||
LoadInitialValue(initialValue);
|
LoadInitialValue(initialValue);
|
||||||
|
|
||||||
|
if (IsIgnoredUsersListField)
|
||||||
|
{
|
||||||
|
SetIgnoredUsersListOptions(ignoredUsersListNames ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsFileNameFormatField)
|
||||||
|
{
|
||||||
|
UpdateFileNameFormatPreview();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PropertyInfo PropertyInfo { get; }
|
public PropertyInfo PropertyInfo { get; }
|
||||||
@ -53,10 +112,25 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
|||||||
|
|
||||||
public bool IsMultiline { get; }
|
public bool IsMultiline { get; }
|
||||||
|
|
||||||
|
public bool IsFileNameFormatField => s_fileNameVariablesByConfigOption.ContainsKey(PropertyName);
|
||||||
|
|
||||||
|
public bool IsIgnoredUsersListField =>
|
||||||
|
string.Equals(PropertyName, nameof(Config.IgnoredUsersListName), StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public bool IsRegularTextInput => IsTextInput && !IsIgnoredUsersListField;
|
||||||
|
|
||||||
|
public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText);
|
||||||
|
|
||||||
public double TextBoxMinHeight => IsMultiline ? 150 : 36;
|
public double TextBoxMinHeight => IsMultiline ? 150 : 36;
|
||||||
|
|
||||||
public ObservableCollection<string> EnumOptions { get; } = [];
|
public ObservableCollection<string> EnumOptions { get; } = [];
|
||||||
|
|
||||||
|
public ObservableCollection<string> AvailableFileNameVariables { get; } = [];
|
||||||
|
|
||||||
|
public ObservableCollection<ConfigSelectOptionViewModel> IgnoredUsersListOptions { get; } = [];
|
||||||
|
|
||||||
|
public ObservableCollection<FileNameFormatSegmentViewModel> FileNameFormatSegments { get; } = [];
|
||||||
|
|
||||||
[ObservableProperty] private bool _boolValue;
|
[ObservableProperty] private bool _boolValue;
|
||||||
|
|
||||||
[ObservableProperty] private string? _enumValue;
|
[ObservableProperty] private string? _enumValue;
|
||||||
@ -67,12 +141,28 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
|||||||
|
|
||||||
[ObservableProperty] private string _textValue = string.Empty;
|
[ObservableProperty] private string _textValue = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(InsertSelectedFileNameVariableCommand))]
|
||||||
|
private string? _selectedFileNameVariable;
|
||||||
|
|
||||||
|
[ObservableProperty] private ConfigSelectOptionViewModel? _selectedIgnoredUsersListOption;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HasHelpText))]
|
||||||
|
private string _helpText = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HasUnknownFileNameVariables))]
|
||||||
|
private string _unknownFileNameVariablesMessage = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(HasError))]
|
[NotifyPropertyChangedFor(nameof(HasError))]
|
||||||
private string _errorMessage = string.Empty;
|
private string _errorMessage = string.Empty;
|
||||||
|
|
||||||
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
||||||
|
|
||||||
|
public bool HasUnknownFileNameVariables => !string.IsNullOrWhiteSpace(UnknownFileNameVariablesMessage);
|
||||||
|
|
||||||
public bool TryGetTypedValue(out object? value, out string? error)
|
public bool TryGetTypedValue(out object? value, out string? error)
|
||||||
{
|
{
|
||||||
value = null;
|
value = null;
|
||||||
@ -192,6 +282,73 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
|||||||
ErrorMessage = message;
|
ErrorMessage = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetIgnoredUsersListOptions(IEnumerable<string> listNames)
|
||||||
|
{
|
||||||
|
if (!IsIgnoredUsersListField)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string selectedValue = SelectedIgnoredUsersListOption?.Value ?? TextValue.Trim();
|
||||||
|
ConfigSelectOptionViewModel noSelectionOption =
|
||||||
|
new(NoListSelectedValue, NoListSelectedDisplayName);
|
||||||
|
|
||||||
|
IgnoredUsersListOptions.Clear();
|
||||||
|
IgnoredUsersListOptions.Add(noSelectionOption);
|
||||||
|
|
||||||
|
IEnumerable<string> distinctNames = listNames
|
||||||
|
.Where(listName => !string.IsNullOrWhiteSpace(listName))
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.OrderBy(listName => listName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (string listName in distinctNames)
|
||||||
|
{
|
||||||
|
IgnoredUsersListOptions.Add(new ConfigSelectOptionViewModel(listName, listName));
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigSelectOptionViewModel selectedOption =
|
||||||
|
IgnoredUsersListOptions.FirstOrDefault(option =>
|
||||||
|
string.Equals(option.Value, selectedValue, StringComparison.Ordinal)) ?? noSelectionOption;
|
||||||
|
|
||||||
|
SelectedIgnoredUsersListOption = selectedOption;
|
||||||
|
TextValue = selectedOption.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanInsertSelectedFileNameVariable))]
|
||||||
|
private void InsertSelectedFileNameVariable()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(SelectedFileNameVariable))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string placeholder = $"{{{SelectedFileNameVariable}}}";
|
||||||
|
TextValue += placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnTextValueChanged(string value)
|
||||||
|
{
|
||||||
|
if (!IsFileNameFormatField)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateFileNameFormatPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanInsertSelectedFileNameVariable() =>
|
||||||
|
IsFileNameFormatField && !string.IsNullOrWhiteSpace(SelectedFileNameVariable);
|
||||||
|
|
||||||
|
partial void OnSelectedIgnoredUsersListOptionChanged(ConfigSelectOptionViewModel? value)
|
||||||
|
{
|
||||||
|
if (!IsIgnoredUsersListField)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextValue = value?.Value ?? NoListSelectedValue;
|
||||||
|
}
|
||||||
|
|
||||||
private void LoadInitialValue(object? initialValue)
|
private void LoadInitialValue(object? initialValue)
|
||||||
{
|
{
|
||||||
if (IsBoolean)
|
if (IsBoolean)
|
||||||
@ -236,6 +393,60 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
|||||||
TextValue = initialValue?.ToString() ?? string.Empty;
|
TextValue = initialValue?.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> GetAllowedFileNameVariables() =>
|
||||||
|
s_fileNameVariablesByConfigOption.TryGetValue(PropertyName, out string[]? variables)
|
||||||
|
? variables
|
||||||
|
: [];
|
||||||
|
|
||||||
|
private void UpdateFileNameFormatPreview()
|
||||||
|
{
|
||||||
|
FileNameFormatSegments.Clear();
|
||||||
|
UnknownFileNameVariablesMessage = string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(TextValue))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<string> allowedVariables = new(GetAllowedFileNameVariables(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
HashSet<string> unknownVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
MatchCollection matches = s_fileNameVariableRegex.Matches(TextValue);
|
||||||
|
int currentIndex = 0;
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
if (match.Index > currentIndex)
|
||||||
|
{
|
||||||
|
string plainText = TextValue[currentIndex..match.Index];
|
||||||
|
FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(plainText, "#1F2A44"));
|
||||||
|
}
|
||||||
|
|
||||||
|
string variableName = match.Groups[1].Value;
|
||||||
|
bool isAllowedVariable = allowedVariables.Contains(variableName);
|
||||||
|
FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(match.Value,
|
||||||
|
isAllowedVariable ? "#2E6EEA" : "#D84E4E"));
|
||||||
|
|
||||||
|
if (!isAllowedVariable)
|
||||||
|
{
|
||||||
|
unknownVariables.Add(variableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = match.Index + match.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex < TextValue.Length)
|
||||||
|
{
|
||||||
|
string trailingText = TextValue[currentIndex..];
|
||||||
|
FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(trailingText, "#1F2A44"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unknownVariables.Count > 0)
|
||||||
|
{
|
||||||
|
string tokens = string.Join(", ", unknownVariables.Select(variable => $"{{{variable}}}"));
|
||||||
|
UnknownFileNameVariablesMessage = $"Unknown variable(s): {tokens}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string ToDisplayName(string propertyName)
|
private static string ToDisplayName(string propertyName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(propertyName))
|
if (string.IsNullOrWhiteSpace(propertyName))
|
||||||
|
|||||||
14
OF DL.Gui/ViewModels/ConfigSelectOptionViewModel.cs
Normal file
14
OF DL.Gui/ViewModels/ConfigSelectOptionViewModel.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
|
public sealed class ConfigSelectOptionViewModel
|
||||||
|
{
|
||||||
|
public ConfigSelectOptionViewModel(string value, string displayName)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
DisplayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public string DisplayName { get; }
|
||||||
|
}
|
||||||
14
OF DL.Gui/ViewModels/FileNameFormatSegmentViewModel.cs
Normal file
14
OF DL.Gui/ViewModels/FileNameFormatSegmentViewModel.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
|
public sealed class FileNameFormatSegmentViewModel
|
||||||
|
{
|
||||||
|
public FileNameFormatSegmentViewModel(string text, string foreground)
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
Foreground = foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Text { get; }
|
||||||
|
|
||||||
|
public string Foreground { get; }
|
||||||
|
}
|
||||||
@ -44,6 +44,85 @@ public partial class MainWindowViewModel(
|
|||||||
("Paid Messages", nameof(Config.DownloadPaidMessages))
|
("Paid Messages", nameof(Config.DownloadPaidMessages))
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string> s_configHelpTextByProperty = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
[nameof(Config.FFmpegPath)] =
|
||||||
|
"Path to the FFmpeg executable. If blank, OF-DL will try the app directory and PATH.",
|
||||||
|
[nameof(Config.DownloadPath)] =
|
||||||
|
"Base download folder. If blank, OF-DL uses __user_data__/sites/OnlyFans/{username}.",
|
||||||
|
[nameof(Config.DownloadVideos)] = "Download video media when enabled.",
|
||||||
|
[nameof(Config.DownloadImages)] = "Download image media when enabled.",
|
||||||
|
[nameof(Config.DownloadAudios)] = "Download audio media when enabled.",
|
||||||
|
[nameof(Config.DownloadAvatarHeaderPhoto)] =
|
||||||
|
"Download creator avatar and header images when enabled.",
|
||||||
|
[nameof(Config.DownloadPosts)] = "Download free posts when enabled.",
|
||||||
|
[nameof(Config.DownloadPaidPosts)] = "Download paid posts when enabled.",
|
||||||
|
[nameof(Config.DownloadArchived)] = "Download archived posts when enabled.",
|
||||||
|
[nameof(Config.DownloadStreams)] = "Download posts from the Streams tab when enabled.",
|
||||||
|
[nameof(Config.DownloadStories)] = "Download stories when enabled.",
|
||||||
|
[nameof(Config.DownloadHighlights)] = "Download highlights when enabled.",
|
||||||
|
[nameof(Config.DownloadMessages)] =
|
||||||
|
"Download free media from messages (including paid-message previews) when enabled.",
|
||||||
|
[nameof(Config.DownloadPaidMessages)] =
|
||||||
|
"Download paid media from messages (excluding preview media) when enabled.",
|
||||||
|
[nameof(Config.IgnoreOwnMessages)] =
|
||||||
|
"Ignore your own sent messages and do not download media sent by your account.",
|
||||||
|
[nameof(Config.DownloadPostsIncrementally)] =
|
||||||
|
"Only download new posts after the latest downloaded post in metadata DB.",
|
||||||
|
[nameof(Config.BypassContentForCreatorsWhoNoLongerExist)] =
|
||||||
|
"Allow downloading accessible purchased content for deleted creators.",
|
||||||
|
[nameof(Config.DownloadDuplicatedMedia)] =
|
||||||
|
"When enabled, duplicate media can be downloaded instead of being skipped.",
|
||||||
|
[nameof(Config.SkipAds)] =
|
||||||
|
"Skip posts/messages containing #ad or free-trial links when enabled.",
|
||||||
|
[nameof(Config.DownloadOnlySpecificDates)] =
|
||||||
|
"Limit downloads by date using DownloadDateSelection and CustomDate.",
|
||||||
|
[nameof(Config.DownloadDateSelection)] =
|
||||||
|
"Choose whether date filtering uses content before or after CustomDate.",
|
||||||
|
[nameof(Config.CustomDate)] =
|
||||||
|
"Date used for date-filtered downloads (yyyy-MM-dd).",
|
||||||
|
[nameof(Config.ShowScrapeSize)] =
|
||||||
|
"Show total byte size instead of item counts during download progress.",
|
||||||
|
[nameof(Config.DisableTextSanitization)] =
|
||||||
|
"Store post/message text as-is without XML stripping.",
|
||||||
|
[nameof(Config.DownloadVideoResolution)] =
|
||||||
|
"Choose preferred video resolution (source, 240, or 720 when available).",
|
||||||
|
[nameof(Config.PaidPostFileNameFormat)] =
|
||||||
|
"Custom filename format for paid posts. See custom filename formats docs.",
|
||||||
|
[nameof(Config.PostFileNameFormat)] =
|
||||||
|
"Custom filename format for free posts/archived/streams. See custom filename formats docs.",
|
||||||
|
[nameof(Config.PaidMessageFileNameFormat)] =
|
||||||
|
"Custom filename format for paid messages. See custom filename formats docs.",
|
||||||
|
[nameof(Config.MessageFileNameFormat)] =
|
||||||
|
"Custom filename format for free messages. See custom filename formats docs.",
|
||||||
|
[nameof(Config.RenameExistingFilesWhenCustomFormatIsSelected)] =
|
||||||
|
"Rename previously downloaded files when custom filename format is enabled.",
|
||||||
|
[nameof(Config.CreatorConfigs)] =
|
||||||
|
"Per-creator filename format overrides. Values here override global filename formats.",
|
||||||
|
[nameof(Config.FolderPerPaidPost)] =
|
||||||
|
"Create a separate folder per paid post when enabled.",
|
||||||
|
[nameof(Config.FolderPerPost)] =
|
||||||
|
"Create a separate folder per free post when enabled.",
|
||||||
|
[nameof(Config.FolderPerPaidMessage)] =
|
||||||
|
"Create a separate folder per paid message when enabled.",
|
||||||
|
[nameof(Config.FolderPerMessage)] =
|
||||||
|
"Create a separate folder per free message when enabled.",
|
||||||
|
[nameof(Config.IncludeExpiredSubscriptions)] =
|
||||||
|
"Include expired subscriptions in user selection.",
|
||||||
|
[nameof(Config.IncludeRestrictedSubscriptions)] =
|
||||||
|
"Include restricted creators in scraping and download flow.",
|
||||||
|
[nameof(Config.IgnoredUsersListName)] =
|
||||||
|
"Users in this list are ignored during scraping. Empty means no users are ignored.",
|
||||||
|
[nameof(Config.Timeout)] =
|
||||||
|
"HTTP timeout override in seconds (-1 uses default behavior).",
|
||||||
|
[nameof(Config.LimitDownloadRate)] =
|
||||||
|
"Enable download speed limiting.",
|
||||||
|
[nameof(Config.DownloadLimitInMbPerSec)] =
|
||||||
|
"Download rate limit in MB/s when rate limiting is enabled.",
|
||||||
|
[nameof(Config.LoggingLevel)] =
|
||||||
|
"Log verbosity written to logs/OFDL.txt."
|
||||||
|
};
|
||||||
|
|
||||||
private Dictionary<string, long> _allUsers = [];
|
private Dictionary<string, long> _allUsers = [];
|
||||||
private Dictionary<string, long> _allLists = [];
|
private Dictionary<string, long> _allLists = [];
|
||||||
private StartupResult _startupResult = new();
|
private StartupResult _startupResult = new();
|
||||||
@ -55,6 +134,10 @@ public partial class MainWindowViewModel(
|
|||||||
|
|
||||||
public ObservableCollection<ConfigCategoryViewModel> ConfigCategories { get; } = [];
|
public ObservableCollection<ConfigCategoryViewModel> ConfigCategories { get; } = [];
|
||||||
|
|
||||||
|
public ObservableCollection<ConfigCategoryViewModel> ConfigCategoriesLeft { get; } = [];
|
||||||
|
|
||||||
|
public ObservableCollection<ConfigCategoryViewModel> ConfigCategoriesRight { get; } = [];
|
||||||
|
|
||||||
public ObservableCollection<MultiSelectOptionViewModel> MediaTypeOptions { get; } = [];
|
public ObservableCollection<MultiSelectOptionViewModel> MediaTypeOptions { get; } = [];
|
||||||
|
|
||||||
public ObservableCollection<MultiSelectOptionViewModel> MediaSourceOptions { get; } = [];
|
public ObservableCollection<MultiSelectOptionViewModel> MediaSourceOptions { get; } = [];
|
||||||
@ -135,6 +218,10 @@ public partial class MainWindowViewModel(
|
|||||||
|
|
||||||
public bool HasMediaSourcesError => !string.IsNullOrWhiteSpace(MediaSourcesError);
|
public bool HasMediaSourcesError => !string.IsNullOrWhiteSpace(MediaSourcesError);
|
||||||
|
|
||||||
|
public string FfmpegPathHelpText => GetConfigHelpText(nameof(Config.FFmpegPath));
|
||||||
|
|
||||||
|
public string DownloadPathHelpText => GetConfigHelpText(nameof(Config.DownloadPath));
|
||||||
|
|
||||||
public string SelectedUsersSummary =>
|
public string SelectedUsersSummary =>
|
||||||
$"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected";
|
$"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected";
|
||||||
|
|
||||||
@ -206,6 +293,7 @@ public partial class MainWindowViewModel(
|
|||||||
DownloadPurchasedTabCommand.NotifyCanExecuteChanged();
|
DownloadPurchasedTabCommand.NotifyCanExecuteChanged();
|
||||||
SelectUsersFromListCommand.NotifyCanExecuteChanged();
|
SelectUsersFromListCommand.NotifyCanExecuteChanged();
|
||||||
RefreshUsersCommand.NotifyCanExecuteChanged();
|
RefreshUsersCommand.NotifyCanExecuteChanged();
|
||||||
|
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@ -248,6 +336,39 @@ public partial class MainWindowViewModel(
|
|||||||
await LoadUsersAndListsAsync();
|
await LoadUsersAndListsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanRefreshIgnoredUsersLists))]
|
||||||
|
private async Task RefreshIgnoredUsersListsAsync(ConfigFieldViewModel? field)
|
||||||
|
{
|
||||||
|
if (field == null || !field.IsIgnoredUsersListField)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigScreenMessage = "Refreshing user lists...";
|
||||||
|
StatusMessage = ConfigScreenMessage;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, long> latestLists = await downloadOrchestrationService.GetUserListsAsync();
|
||||||
|
_allLists = latestLists
|
||||||
|
.OrderBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||||
|
|
||||||
|
UpdateUserListsCollection();
|
||||||
|
UpdateIgnoredUsersListFieldOptions();
|
||||||
|
|
||||||
|
ConfigScreenMessage = $"User lists refreshed ({_allLists.Count}).";
|
||||||
|
StatusMessage = ConfigScreenMessage;
|
||||||
|
AppendLog(ConfigScreenMessage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConfigScreenMessage = $"Could not refresh user lists: {ex.Message}";
|
||||||
|
StatusMessage = ConfigScreenMessage;
|
||||||
|
AppendLog(ConfigScreenMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SaveConfigAsync()
|
private async Task SaveConfigAsync()
|
||||||
{
|
{
|
||||||
@ -510,6 +631,9 @@ public partial class MainWindowViewModel(
|
|||||||
private bool CanRefreshUsers() =>
|
private bool CanRefreshUsers() =>
|
||||||
CurrentScreen == AppScreen.UserSelection && !IsDownloading;
|
CurrentScreen == AppScreen.UserSelection && !IsDownloading;
|
||||||
|
|
||||||
|
private bool CanRefreshIgnoredUsersLists() =>
|
||||||
|
CurrentScreen == AppScreen.Config && IsAuthenticated && !IsDownloading;
|
||||||
|
|
||||||
private bool CanLogout() => IsAuthenticated && !IsDownloading;
|
private bool CanLogout() => IsAuthenticated && !IsDownloading;
|
||||||
|
|
||||||
partial void OnCurrentScreenChanged(AppScreen value)
|
partial void OnCurrentScreenChanged(AppScreen value)
|
||||||
@ -525,6 +649,7 @@ public partial class MainWindowViewModel(
|
|||||||
SelectUsersFromListCommand.NotifyCanExecuteChanged();
|
SelectUsersFromListCommand.NotifyCanExecuteChanged();
|
||||||
StopWorkCommand.NotifyCanExecuteChanged();
|
StopWorkCommand.NotifyCanExecuteChanged();
|
||||||
RefreshUsersCommand.NotifyCanExecuteChanged();
|
RefreshUsersCommand.NotifyCanExecuteChanged();
|
||||||
|
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||||
LogoutCommand.NotifyCanExecuteChanged();
|
LogoutCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -535,12 +660,14 @@ public partial class MainWindowViewModel(
|
|||||||
SelectUsersFromListCommand.NotifyCanExecuteChanged();
|
SelectUsersFromListCommand.NotifyCanExecuteChanged();
|
||||||
StopWorkCommand.NotifyCanExecuteChanged();
|
StopWorkCommand.NotifyCanExecuteChanged();
|
||||||
RefreshUsersCommand.NotifyCanExecuteChanged();
|
RefreshUsersCommand.NotifyCanExecuteChanged();
|
||||||
|
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||||
LogoutCommand.NotifyCanExecuteChanged();
|
LogoutCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnIsAuthenticatedChanged(bool value)
|
partial void OnIsAuthenticatedChanged(bool value)
|
||||||
{
|
{
|
||||||
LogoutCommand.NotifyCanExecuteChanged();
|
LogoutCommand.NotifyCanExecuteChanged();
|
||||||
|
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedListNameChanged(string? value)
|
partial void OnSelectedListNameChanged(string? value)
|
||||||
@ -692,8 +819,12 @@ public partial class MainWindowViewModel(
|
|||||||
SetLoading("Fetching users and user lists...");
|
SetLoading("Fetching users and user lists...");
|
||||||
UserListResult listResult = await downloadOrchestrationService.GetAvailableUsersAsync();
|
UserListResult listResult = await downloadOrchestrationService.GetAvailableUsersAsync();
|
||||||
|
|
||||||
_allUsers = listResult.Users.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value);
|
_allUsers = listResult.Users
|
||||||
_allLists = listResult.Lists.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value);
|
.OrderBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||||
|
_allLists = listResult.Lists
|
||||||
|
.OrderBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||||
|
|
||||||
foreach (SelectableUserViewModel user in AvailableUsers)
|
foreach (SelectableUserViewModel user in AvailableUsers)
|
||||||
{
|
{
|
||||||
@ -709,11 +840,8 @@ public partial class MainWindowViewModel(
|
|||||||
}
|
}
|
||||||
OnPropertyChanged(nameof(SelectedUsersSummary));
|
OnPropertyChanged(nameof(SelectedUsersSummary));
|
||||||
|
|
||||||
UserLists.Clear();
|
UpdateUserListsCollection();
|
||||||
foreach (string listName in _allLists.Keys)
|
UpdateIgnoredUsersListFieldOptions();
|
||||||
{
|
|
||||||
UserLists.Add(listName);
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectedListName = null;
|
SelectedListName = null;
|
||||||
|
|
||||||
@ -809,6 +937,8 @@ public partial class MainWindowViewModel(
|
|||||||
{
|
{
|
||||||
ConfigFields.Clear();
|
ConfigFields.Clear();
|
||||||
ConfigCategories.Clear();
|
ConfigCategories.Clear();
|
||||||
|
ConfigCategoriesLeft.Clear();
|
||||||
|
ConfigCategoriesRight.Clear();
|
||||||
BuildSpecialConfigInputs(config);
|
BuildSpecialConfigInputs(config);
|
||||||
|
|
||||||
IEnumerable<System.Reflection.PropertyInfo> properties = typeof(Config)
|
IEnumerable<System.Reflection.PropertyInfo> properties = typeof(Config)
|
||||||
@ -820,7 +950,14 @@ public partial class MainWindowViewModel(
|
|||||||
foreach (System.Reflection.PropertyInfo property in properties)
|
foreach (System.Reflection.PropertyInfo property in properties)
|
||||||
{
|
{
|
||||||
object? value = property.GetValue(config);
|
object? value = property.GetValue(config);
|
||||||
ConfigFields.Add(new ConfigFieldViewModel(property, value));
|
IEnumerable<string> ignoredUsersListNames = property.Name == nameof(Config.IgnoredUsersListName)
|
||||||
|
? _allLists.Keys
|
||||||
|
: [];
|
||||||
|
ConfigFields.Add(new ConfigFieldViewModel(
|
||||||
|
property,
|
||||||
|
value,
|
||||||
|
ignoredUsersListNames,
|
||||||
|
GetConfigHelpText(property.Name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<IGrouping<string, ConfigFieldViewModel>> grouped = ConfigFields
|
IEnumerable<IGrouping<string, ConfigFieldViewModel>> grouped = ConfigFields
|
||||||
@ -828,9 +965,41 @@ public partial class MainWindowViewModel(
|
|||||||
.OrderBy(group => GetCategoryOrder(group.Key))
|
.OrderBy(group => GetCategoryOrder(group.Key))
|
||||||
.ThenBy(group => group.Key);
|
.ThenBy(group => group.Key);
|
||||||
|
|
||||||
|
int categoryIndex = 0;
|
||||||
foreach (IGrouping<string, ConfigFieldViewModel> group in grouped)
|
foreach (IGrouping<string, ConfigFieldViewModel> group in grouped)
|
||||||
{
|
{
|
||||||
ConfigCategories.Add(new ConfigCategoryViewModel(group.Key, group.OrderBy(field => field.DisplayName)));
|
ConfigCategoryViewModel category = new(group.Key, group.OrderBy(field => field.DisplayName));
|
||||||
|
ConfigCategories.Add(category);
|
||||||
|
if (categoryIndex % 2 == 0)
|
||||||
|
{
|
||||||
|
ConfigCategoriesLeft.Add(category);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConfigCategoriesRight.Add(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateIgnoredUsersListFieldOptions()
|
||||||
|
{
|
||||||
|
IEnumerable<string> listNames = _allLists.Keys
|
||||||
|
.OrderBy(listName => listName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (ConfigFieldViewModel field in ConfigFields.Where(field => field.IsIgnoredUsersListField))
|
||||||
|
{
|
||||||
|
field.SetIgnoredUsersListOptions(listNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateUserListsCollection()
|
||||||
|
{
|
||||||
|
UserLists.Clear();
|
||||||
|
foreach (string listName in _allLists.Keys.OrderBy(name => name, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
UserLists.Add(listName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -855,7 +1024,8 @@ public partial class MainWindowViewModel(
|
|||||||
foreach ((string displayName, string propertyName) in definitions)
|
foreach ((string displayName, string propertyName) in definitions)
|
||||||
{
|
{
|
||||||
options.Add(new MultiSelectOptionViewModel(displayName, propertyName,
|
options.Add(new MultiSelectOptionViewModel(displayName, propertyName,
|
||||||
GetBooleanConfigValue(config, propertyName)));
|
GetBooleanConfigValue(config, propertyName),
|
||||||
|
GetConfigHelpText(propertyName)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -954,6 +1124,11 @@ public partial class MainWindowViewModel(
|
|||||||
private static string EscapePathForConfig(string path) =>
|
private static string EscapePathForConfig(string path) =>
|
||||||
path.Replace(@"\", @"\\");
|
path.Replace(@"\", @"\\");
|
||||||
|
|
||||||
|
private static string GetConfigHelpText(string propertyName) =>
|
||||||
|
s_configHelpTextByProperty.TryGetValue(propertyName, out string? helpText)
|
||||||
|
? helpText
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
private void SubscribeSpecialSelectionEvents()
|
private void SubscribeSpecialSelectionEvents()
|
||||||
{
|
{
|
||||||
foreach (MultiSelectOptionViewModel option in MediaTypeOptions)
|
foreach (MultiSelectOptionViewModel option in MediaTypeOptions)
|
||||||
|
|||||||
@ -4,16 +4,21 @@ namespace OF_DL.Gui.ViewModels;
|
|||||||
|
|
||||||
public partial class MultiSelectOptionViewModel : ViewModelBase
|
public partial class MultiSelectOptionViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public MultiSelectOptionViewModel(string displayName, string propertyName, bool isSelected)
|
public MultiSelectOptionViewModel(string displayName, string propertyName, bool isSelected, string? helpText = null)
|
||||||
{
|
{
|
||||||
DisplayName = displayName;
|
DisplayName = displayName;
|
||||||
PropertyName = propertyName;
|
PropertyName = propertyName;
|
||||||
IsSelected = isSelected;
|
IsSelected = isSelected;
|
||||||
|
HelpText = helpText?.Trim() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DisplayName { get; }
|
public string DisplayName { get; }
|
||||||
|
|
||||||
public string PropertyName { get; }
|
public string PropertyName { get; }
|
||||||
|
|
||||||
|
public string HelpText { get; }
|
||||||
|
|
||||||
|
public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText);
|
||||||
|
|
||||||
[ObservableProperty] private bool _isSelected;
|
[ObservableProperty] private bool _isSelected;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,9 +105,26 @@
|
|||||||
<Border Grid.Column="0" Classes="surface" Padding="12" Margin="0,0,10,0">
|
<Border Grid.Column="0" Classes="surface" Padding="12" Margin="0,0,10,0">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
<TextBlock FontSize="16" FontWeight="Bold" Foreground="#1F2A44" Text="External" />
|
<TextBlock FontSize="16" FontWeight="Bold" Foreground="#1F2A44" Text="External" />
|
||||||
<TextBlock FontWeight="SemiBold"
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
Foreground="#1F2A44"
|
<TextBlock FontWeight="SemiBold"
|
||||||
Text="FFmpeg Path" />
|
Foreground="#1F2A44"
|
||||||
|
Text="FFmpeg Path" />
|
||||||
|
<Button Width="20"
|
||||||
|
Height="20"
|
||||||
|
MinWidth="20"
|
||||||
|
MinHeight="20"
|
||||||
|
Padding="0"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Background="#EAF0FB"
|
||||||
|
BorderBrush="#C5D4EC"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="10"
|
||||||
|
Content="?"
|
||||||
|
ToolTip.Tip="{Binding FfmpegPathHelpText}" />
|
||||||
|
</StackPanel>
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBox Grid.Column="0"
|
<TextBox Grid.Column="0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@ -143,9 +160,26 @@
|
|||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:MultiSelectOptionViewModel">
|
<DataTemplate x:DataType="vm:MultiSelectOptionViewModel">
|
||||||
<CheckBox Margin="0,0,14,6"
|
<StackPanel Margin="0,0,14,6" Orientation="Horizontal" Spacing="6">
|
||||||
Content="{Binding DisplayName}"
|
<CheckBox Content="{Binding DisplayName}"
|
||||||
IsChecked="{Binding IsSelected}" />
|
IsChecked="{Binding IsSelected}" />
|
||||||
|
<Button IsVisible="{Binding HasHelpText}"
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
MinWidth="18"
|
||||||
|
MinHeight="18"
|
||||||
|
Padding="0"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="11"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Background="#EAF0FB"
|
||||||
|
BorderBrush="#C5D4EC"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="9"
|
||||||
|
Content="?"
|
||||||
|
ToolTip.Tip="{Binding HelpText}" />
|
||||||
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
@ -165,9 +199,26 @@
|
|||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:MultiSelectOptionViewModel">
|
<DataTemplate x:DataType="vm:MultiSelectOptionViewModel">
|
||||||
<CheckBox Margin="0,0,14,6"
|
<StackPanel Margin="0,0,14,6" Orientation="Horizontal" Spacing="6">
|
||||||
Content="{Binding DisplayName}"
|
<CheckBox Content="{Binding DisplayName}"
|
||||||
IsChecked="{Binding IsSelected}" />
|
IsChecked="{Binding IsSelected}" />
|
||||||
|
<Button IsVisible="{Binding HasHelpText}"
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
MinWidth="18"
|
||||||
|
MinHeight="18"
|
||||||
|
Padding="0"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="11"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Background="#EAF0FB"
|
||||||
|
BorderBrush="#C5D4EC"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="9"
|
||||||
|
Content="?"
|
||||||
|
ToolTip.Tip="{Binding HelpText}" />
|
||||||
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
@ -181,17 +232,16 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Border Classes="surface" Padding="12">
|
<Border Classes="surface" Padding="12">
|
||||||
<ItemsControl ItemsSource="{Binding ConfigCategories}">
|
<Grid ColumnDefinitions="*,*">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl x:Name="LeftConfigCategories"
|
||||||
<ItemsPanelTemplate>
|
Grid.Column="0"
|
||||||
<WrapPanel Orientation="Horizontal" />
|
Margin="0,0,6,0"
|
||||||
</ItemsPanelTemplate>
|
ItemsSource="{Binding ConfigCategoriesLeft}">
|
||||||
</ItemsControl.ItemsPanel>
|
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:ConfigCategoryViewModel">
|
<DataTemplate x:DataType="vm:ConfigCategoryViewModel">
|
||||||
<Border Classes="surface"
|
<Border Classes="surface"
|
||||||
Width="600"
|
Margin="0,0,0,12"
|
||||||
Margin="0,0,12,12"
|
HorizontalAlignment="Stretch"
|
||||||
Padding="10">
|
Padding="10">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
<TextBlock FontSize="16"
|
<TextBlock FontSize="16"
|
||||||
@ -202,9 +252,26 @@
|
|||||||
Spacing="4"
|
Spacing="4"
|
||||||
Margin="0,0,0,10"
|
Margin="0,0,0,10"
|
||||||
x:CompileBindings="False">
|
x:CompileBindings="False">
|
||||||
<TextBlock FontWeight="SemiBold"
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
Foreground="#1F2A44"
|
<TextBlock FontWeight="SemiBold"
|
||||||
Text="Download Path" />
|
Foreground="#1F2A44"
|
||||||
|
Text="Download Path" />
|
||||||
|
<Button Width="20"
|
||||||
|
Height="20"
|
||||||
|
MinWidth="20"
|
||||||
|
MinHeight="20"
|
||||||
|
Padding="0"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Background="#EAF0FB"
|
||||||
|
BorderBrush="#C5D4EC"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="10"
|
||||||
|
Content="?"
|
||||||
|
ToolTip.Tip="{Binding DataContext.DownloadPathHelpText, RelativeSource={RelativeSource AncestorType=Window}}" />
|
||||||
|
</StackPanel>
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBox Grid.Column="0"
|
<TextBox Grid.Column="0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@ -222,16 +289,98 @@
|
|||||||
Text="{Binding DataContext.DownloadPathError, RelativeSource={RelativeSource AncestorType=Window}}"
|
Text="{Binding DataContext.DownloadPathError, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel IsVisible="{Binding HasSpecificDateFilterFields}"
|
||||||
|
Margin="0,0,0,6"
|
||||||
|
Spacing="6"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
x:CompileBindings="False">
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
Spacing="6"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock FontWeight="SemiBold"
|
||||||
|
Foreground="#1F2A44"
|
||||||
|
Text="Download Posts from Specific Dates"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<Button IsVisible="{Binding HasSpecificDateFilterHelpText}"
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
MinWidth="20"
|
||||||
|
MinHeight="20"
|
||||||
|
Padding="0"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Background="#EAF0FB"
|
||||||
|
BorderBrush="#C5D4EC"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="10"
|
||||||
|
Content="?"
|
||||||
|
ToolTip.Tip="{Binding SpecificDateFilterHelpText}" />
|
||||||
|
</StackPanel>
|
||||||
|
<Grid ColumnDefinitions="Auto,Auto,*"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<CheckBox Grid.Column="0"
|
||||||
|
Content="Enable"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
IsChecked="{Binding DownloadOnlySpecificDatesField.BoolValue}" />
|
||||||
|
<ComboBox Grid.Column="1"
|
||||||
|
Width="140"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
IsEnabled="{Binding DownloadOnlySpecificDatesField.BoolValue}"
|
||||||
|
ItemsSource="{Binding DownloadDateSelectionField.EnumOptions}"
|
||||||
|
SelectedItem="{Binding DownloadDateSelectionField.EnumValue}" />
|
||||||
|
<DatePicker Grid.Column="2"
|
||||||
|
MinWidth="320"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsEnabled="{Binding DownloadOnlySpecificDatesField.BoolValue}"
|
||||||
|
SelectedDate="{Binding CustomDateField.DateValue}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
<ItemsControl ItemsSource="{Binding Fields}">
|
<ItemsControl ItemsSource="{Binding Fields}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Spacing="10" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:ConfigFieldViewModel">
|
<DataTemplate x:DataType="vm:ConfigFieldViewModel">
|
||||||
<Grid Margin="0,0,0,10" ColumnDefinitions="190,*">
|
<Grid Margin="0" ColumnDefinitions="190,*">
|
||||||
<TextBlock Grid.Column="0"
|
<Grid Grid.Column="0"
|
||||||
Margin="0,6,10,0"
|
ColumnDefinitions="*,Auto"
|
||||||
FontWeight="SemiBold"
|
Margin="0,6,10,0"
|
||||||
Foreground="#1F2A44"
|
ClipToBounds="True">
|
||||||
Text="{Binding DisplayName}"
|
<TextBlock Grid.Column="0"
|
||||||
TextWrapping="Wrap" />
|
FontWeight="SemiBold"
|
||||||
|
Foreground="#1F2A44"
|
||||||
|
Text="{Binding DisplayName}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
VerticalAlignment="Top" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
IsVisible="{Binding HasHelpText}"
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
MinWidth="20"
|
||||||
|
MinHeight="20"
|
||||||
|
Margin="6,0,0,0"
|
||||||
|
Padding="0"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Background="#EAF0FB"
|
||||||
|
BorderBrush="#C5D4EC"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="10"
|
||||||
|
Content="?"
|
||||||
|
ToolTip.Tip="{Binding HelpText}" />
|
||||||
|
</Grid>
|
||||||
<StackPanel Grid.Column="1" Spacing="4">
|
<StackPanel Grid.Column="1" Spacing="4">
|
||||||
<CheckBox IsVisible="{Binding IsBoolean}"
|
<CheckBox IsVisible="{Binding IsBoolean}"
|
||||||
IsChecked="{Binding BoolValue}" />
|
IsChecked="{Binding BoolValue}" />
|
||||||
@ -251,13 +400,90 @@
|
|||||||
Increment="1"
|
Increment="1"
|
||||||
FormatString="N0" />
|
FormatString="N0" />
|
||||||
|
|
||||||
<TextBox IsVisible="{Binding IsTextInput}"
|
<TextBox IsVisible="{Binding IsRegularTextInput}"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
AcceptsReturn="{Binding IsMultiline}"
|
AcceptsReturn="{Binding IsMultiline}"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
MinHeight="{Binding TextBoxMinHeight}"
|
MinHeight="{Binding TextBoxMinHeight}"
|
||||||
Text="{Binding TextValue}" />
|
Text="{Binding TextValue}" />
|
||||||
|
|
||||||
|
<Grid IsVisible="{Binding IsIgnoredUsersListField}"
|
||||||
|
ColumnDefinitions="*,Auto"
|
||||||
|
x:CompileBindings="False">
|
||||||
|
<ComboBox Grid.Column="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ItemsSource="{Binding IgnoredUsersListOptions}"
|
||||||
|
SelectedItem="{Binding SelectedIgnoredUsersListOption}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ConfigSelectOptionViewModel">
|
||||||
|
<TextBlock Text="{Binding DisplayName}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Width="34"
|
||||||
|
Height="34"
|
||||||
|
MinWidth="34"
|
||||||
|
MinHeight="34"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
Padding="0"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
Background="#EAF0FB"
|
||||||
|
BorderBrush="#C5D4EC"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8"
|
||||||
|
Content="⟳"
|
||||||
|
ToolTip.Tip="Refresh list names from OnlyFans"
|
||||||
|
Command="{Binding DataContext.RefreshIgnoredUsersListsCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
CommandParameter="{Binding}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel IsVisible="{Binding IsFileNameFormatField}"
|
||||||
|
Spacing="6">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<ComboBox Grid.Column="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ItemsSource="{Binding AvailableFileNameVariables}"
|
||||||
|
SelectedItem="{Binding SelectedFileNameVariable}" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
Classes="secondary"
|
||||||
|
Content="Insert Variable"
|
||||||
|
Command="{Binding InsertSelectedFileNameVariableCommand}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Padding="8"
|
||||||
|
Background="#F5F8FE"
|
||||||
|
BorderBrush="#D8E3F4"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8">
|
||||||
|
<ItemsControl ItemsSource="{Binding FileNameFormatSegments}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:FileNameFormatSegmentViewModel">
|
||||||
|
<TextBlock Text="{Binding Text}"
|
||||||
|
Foreground="{Binding Foreground}"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock IsVisible="{Binding HasUnknownFileNameVariables}"
|
||||||
|
Foreground="#FF5A5A"
|
||||||
|
Text="{Binding UnknownFileNameVariablesMessage}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock IsVisible="{Binding HasError}"
|
<TextBlock IsVisible="{Binding HasError}"
|
||||||
Foreground="#FF5A5A"
|
Foreground="#FF5A5A"
|
||||||
Text="{Binding ErrorMessage}"
|
Text="{Binding ErrorMessage}"
|
||||||
@ -272,6 +498,11 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
<ItemsControl Grid.Column="1"
|
||||||
|
Margin="6,0,0,0"
|
||||||
|
ItemsSource="{Binding ConfigCategoriesRight}"
|
||||||
|
ItemTemplate="{Binding ItemTemplate, ElementName=LeftConfigCategories}" />
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user