UI improvements to the configuration page
This commit is contained in:
parent
ec8bf47de5
commit
712f11dc4b
@ -96,6 +96,13 @@ public class DownloadOrchestrationService(
|
||||
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>
|
||||
/// Resolves the users that belong to a specific list.
|
||||
/// </summary>
|
||||
|
||||
@ -9,6 +9,11 @@ public interface IDownloadOrchestrationService
|
||||
/// </summary>
|
||||
Task<UserListResult> GetAvailableUsersAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Fetch only user lists.
|
||||
/// </summary>
|
||||
Task<Dictionary<string, long>> GetUserListsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get users for a specific list by name.
|
||||
/// </summary>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using OF_DL.Models.Config;
|
||||
|
||||
namespace OF_DL.Gui.ViewModels;
|
||||
|
||||
@ -7,7 +8,22 @@ public sealed class ConfigCategoryViewModel : ViewModelBase
|
||||
public ConfigCategoryViewModel(string categoryName, IEnumerable<ConfigFieldViewModel> fields)
|
||||
{
|
||||
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);
|
||||
}
|
||||
@ -18,5 +34,42 @@ public sealed class ConfigCategoryViewModel : ViewModelBase
|
||||
public bool IsDownloadBehavior =>
|
||||
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; } = [];
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Newtonsoft.Json;
|
||||
using OF_DL.Models.Config;
|
||||
|
||||
@ -8,12 +10,43 @@ namespace OF_DL.Gui.ViewModels;
|
||||
|
||||
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;
|
||||
PropertyName = propertyInfo.Name;
|
||||
DisplayName = ToDisplayName(propertyInfo.Name);
|
||||
PropertyType = propertyInfo.PropertyType;
|
||||
HelpText = helpText?.Trim() ?? string.Empty;
|
||||
|
||||
IsBoolean = PropertyType == typeof(bool);
|
||||
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);
|
||||
|
||||
if (IsIgnoredUsersListField)
|
||||
{
|
||||
SetIgnoredUsersListOptions(ignoredUsersListNames ?? []);
|
||||
}
|
||||
|
||||
if (IsFileNameFormatField)
|
||||
{
|
||||
UpdateFileNameFormatPreview();
|
||||
}
|
||||
}
|
||||
|
||||
public PropertyInfo PropertyInfo { get; }
|
||||
@ -53,10 +112,25 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
||||
|
||||
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 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 string? _enumValue;
|
||||
@ -67,12 +141,28 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
||||
|
||||
[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]
|
||||
[NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string _errorMessage = string.Empty;
|
||||
|
||||
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
||||
|
||||
public bool HasUnknownFileNameVariables => !string.IsNullOrWhiteSpace(UnknownFileNameVariablesMessage);
|
||||
|
||||
public bool TryGetTypedValue(out object? value, out string? error)
|
||||
{
|
||||
value = null;
|
||||
@ -192,6 +282,73 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
||||
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)
|
||||
{
|
||||
if (IsBoolean)
|
||||
@ -236,6 +393,60 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
||||
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)
|
||||
{
|
||||
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))
|
||||
];
|
||||
|
||||
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> _allLists = [];
|
||||
private StartupResult _startupResult = new();
|
||||
@ -55,6 +134,10 @@ public partial class MainWindowViewModel(
|
||||
|
||||
public ObservableCollection<ConfigCategoryViewModel> ConfigCategories { get; } = [];
|
||||
|
||||
public ObservableCollection<ConfigCategoryViewModel> ConfigCategoriesLeft { get; } = [];
|
||||
|
||||
public ObservableCollection<ConfigCategoryViewModel> ConfigCategoriesRight { get; } = [];
|
||||
|
||||
public ObservableCollection<MultiSelectOptionViewModel> MediaTypeOptions { get; } = [];
|
||||
|
||||
public ObservableCollection<MultiSelectOptionViewModel> MediaSourceOptions { get; } = [];
|
||||
@ -135,6 +218,10 @@ public partial class MainWindowViewModel(
|
||||
|
||||
public bool HasMediaSourcesError => !string.IsNullOrWhiteSpace(MediaSourcesError);
|
||||
|
||||
public string FfmpegPathHelpText => GetConfigHelpText(nameof(Config.FFmpegPath));
|
||||
|
||||
public string DownloadPathHelpText => GetConfigHelpText(nameof(Config.DownloadPath));
|
||||
|
||||
public string SelectedUsersSummary =>
|
||||
$"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected";
|
||||
|
||||
@ -206,6 +293,7 @@ public partial class MainWindowViewModel(
|
||||
DownloadPurchasedTabCommand.NotifyCanExecuteChanged();
|
||||
SelectUsersFromListCommand.NotifyCanExecuteChanged();
|
||||
RefreshUsersCommand.NotifyCanExecuteChanged();
|
||||
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@ -248,6 +336,39 @@ public partial class MainWindowViewModel(
|
||||
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]
|
||||
private async Task SaveConfigAsync()
|
||||
{
|
||||
@ -510,6 +631,9 @@ public partial class MainWindowViewModel(
|
||||
private bool CanRefreshUsers() =>
|
||||
CurrentScreen == AppScreen.UserSelection && !IsDownloading;
|
||||
|
||||
private bool CanRefreshIgnoredUsersLists() =>
|
||||
CurrentScreen == AppScreen.Config && IsAuthenticated && !IsDownloading;
|
||||
|
||||
private bool CanLogout() => IsAuthenticated && !IsDownloading;
|
||||
|
||||
partial void OnCurrentScreenChanged(AppScreen value)
|
||||
@ -525,6 +649,7 @@ public partial class MainWindowViewModel(
|
||||
SelectUsersFromListCommand.NotifyCanExecuteChanged();
|
||||
StopWorkCommand.NotifyCanExecuteChanged();
|
||||
RefreshUsersCommand.NotifyCanExecuteChanged();
|
||||
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||
LogoutCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
@ -535,12 +660,14 @@ public partial class MainWindowViewModel(
|
||||
SelectUsersFromListCommand.NotifyCanExecuteChanged();
|
||||
StopWorkCommand.NotifyCanExecuteChanged();
|
||||
RefreshUsersCommand.NotifyCanExecuteChanged();
|
||||
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||
LogoutCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
partial void OnIsAuthenticatedChanged(bool value)
|
||||
{
|
||||
LogoutCommand.NotifyCanExecuteChanged();
|
||||
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
partial void OnSelectedListNameChanged(string? value)
|
||||
@ -692,8 +819,12 @@ public partial class MainWindowViewModel(
|
||||
SetLoading("Fetching users and user lists...");
|
||||
UserListResult listResult = await downloadOrchestrationService.GetAvailableUsersAsync();
|
||||
|
||||
_allUsers = listResult.Users.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||
_allLists = listResult.Lists.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||
_allUsers = listResult.Users
|
||||
.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)
|
||||
{
|
||||
@ -709,11 +840,8 @@ public partial class MainWindowViewModel(
|
||||
}
|
||||
OnPropertyChanged(nameof(SelectedUsersSummary));
|
||||
|
||||
UserLists.Clear();
|
||||
foreach (string listName in _allLists.Keys)
|
||||
{
|
||||
UserLists.Add(listName);
|
||||
}
|
||||
UpdateUserListsCollection();
|
||||
UpdateIgnoredUsersListFieldOptions();
|
||||
|
||||
SelectedListName = null;
|
||||
|
||||
@ -809,6 +937,8 @@ public partial class MainWindowViewModel(
|
||||
{
|
||||
ConfigFields.Clear();
|
||||
ConfigCategories.Clear();
|
||||
ConfigCategoriesLeft.Clear();
|
||||
ConfigCategoriesRight.Clear();
|
||||
BuildSpecialConfigInputs(config);
|
||||
|
||||
IEnumerable<System.Reflection.PropertyInfo> properties = typeof(Config)
|
||||
@ -820,7 +950,14 @@ public partial class MainWindowViewModel(
|
||||
foreach (System.Reflection.PropertyInfo property in properties)
|
||||
{
|
||||
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
|
||||
@ -828,9 +965,41 @@ public partial class MainWindowViewModel(
|
||||
.OrderBy(group => GetCategoryOrder(group.Key))
|
||||
.ThenBy(group => group.Key);
|
||||
|
||||
int categoryIndex = 0;
|
||||
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)
|
||||
{
|
||||
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) =>
|
||||
path.Replace(@"\", @"\\");
|
||||
|
||||
private static string GetConfigHelpText(string propertyName) =>
|
||||
s_configHelpTextByProperty.TryGetValue(propertyName, out string? helpText)
|
||||
? helpText
|
||||
: string.Empty;
|
||||
|
||||
private void SubscribeSpecialSelectionEvents()
|
||||
{
|
||||
foreach (MultiSelectOptionViewModel option in MediaTypeOptions)
|
||||
|
||||
@ -4,16 +4,21 @@ namespace OF_DL.Gui.ViewModels;
|
||||
|
||||
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;
|
||||
PropertyName = propertyName;
|
||||
IsSelected = isSelected;
|
||||
HelpText = helpText?.Trim() ?? string.Empty;
|
||||
}
|
||||
|
||||
public string DisplayName { get; }
|
||||
|
||||
public string PropertyName { get; }
|
||||
|
||||
public string HelpText { get; }
|
||||
|
||||
public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText);
|
||||
|
||||
[ObservableProperty] private bool _isSelected;
|
||||
}
|
||||
|
||||
@ -105,9 +105,26 @@
|
||||
<Border Grid.Column="0" Classes="surface" Padding="12" Margin="0,0,10,0">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock FontSize="16" FontWeight="Bold" Foreground="#1F2A44" Text="External" />
|
||||
<TextBlock FontWeight="SemiBold"
|
||||
Foreground="#1F2A44"
|
||||
Text="FFmpeg Path" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock FontWeight="SemiBold"
|
||||
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">
|
||||
<TextBox Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
@ -143,9 +160,26 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:MultiSelectOptionViewModel">
|
||||
<CheckBox Margin="0,0,14,6"
|
||||
Content="{Binding DisplayName}"
|
||||
IsChecked="{Binding IsSelected}" />
|
||||
<StackPanel Margin="0,0,14,6" Orientation="Horizontal" Spacing="6">
|
||||
<CheckBox Content="{Binding DisplayName}"
|
||||
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>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
@ -165,9 +199,26 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:MultiSelectOptionViewModel">
|
||||
<CheckBox Margin="0,0,14,6"
|
||||
Content="{Binding DisplayName}"
|
||||
IsChecked="{Binding IsSelected}" />
|
||||
<StackPanel Margin="0,0,14,6" Orientation="Horizontal" Spacing="6">
|
||||
<CheckBox Content="{Binding DisplayName}"
|
||||
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>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
@ -181,17 +232,16 @@
|
||||
</Grid>
|
||||
|
||||
<Border Classes="surface" Padding="12">
|
||||
<ItemsControl ItemsSource="{Binding ConfigCategories}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<ItemsControl x:Name="LeftConfigCategories"
|
||||
Grid.Column="0"
|
||||
Margin="0,0,6,0"
|
||||
ItemsSource="{Binding ConfigCategoriesLeft}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ConfigCategoryViewModel">
|
||||
<Border Classes="surface"
|
||||
Width="600"
|
||||
Margin="0,0,12,12"
|
||||
Margin="0,0,0,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="10">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock FontSize="16"
|
||||
@ -202,9 +252,26 @@
|
||||
Spacing="4"
|
||||
Margin="0,0,0,10"
|
||||
x:CompileBindings="False">
|
||||
<TextBlock FontWeight="SemiBold"
|
||||
Foreground="#1F2A44"
|
||||
Text="Download Path" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock FontWeight="SemiBold"
|
||||
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">
|
||||
<TextBox Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
@ -222,16 +289,98 @@
|
||||
Text="{Binding DataContext.DownloadPathError, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
TextWrapping="Wrap" />
|
||||
</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.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="10" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ConfigFieldViewModel">
|
||||
<Grid Margin="0,0,0,10" ColumnDefinitions="190,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Margin="0,6,10,0"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#1F2A44"
|
||||
Text="{Binding DisplayName}"
|
||||
TextWrapping="Wrap" />
|
||||
<Grid Margin="0" ColumnDefinitions="190,*">
|
||||
<Grid Grid.Column="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="0,6,10,0"
|
||||
ClipToBounds="True">
|
||||
<TextBlock Grid.Column="0"
|
||||
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">
|
||||
<CheckBox IsVisible="{Binding IsBoolean}"
|
||||
IsChecked="{Binding BoolValue}" />
|
||||
@ -251,13 +400,90 @@
|
||||
Increment="1"
|
||||
FormatString="N0" />
|
||||
|
||||
<TextBox IsVisible="{Binding IsTextInput}"
|
||||
<TextBox IsVisible="{Binding IsRegularTextInput}"
|
||||
HorizontalAlignment="Stretch"
|
||||
AcceptsReturn="{Binding IsMultiline}"
|
||||
TextWrapping="Wrap"
|
||||
MinHeight="{Binding TextBoxMinHeight}"
|
||||
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}"
|
||||
Foreground="#FF5A5A"
|
||||
Text="{Binding ErrorMessage}"
|
||||
@ -272,6 +498,11 @@
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ItemsControl Grid.Column="1"
|
||||
Margin="6,0,0,0"
|
||||
ItemsSource="{Binding ConfigCategoriesRight}"
|
||||
ItemTemplate="{Binding ItemTemplate, ElementName=LeftConfigCategories}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user