Improve the filename format input field's appearance and validation

This commit is contained in:
whimsical-c4lic0 2026-02-17 13:58:06 -06:00
parent b6872a2b9e
commit e58ac7d2a6
4 changed files with 516 additions and 135 deletions

View File

@ -0,0 +1,224 @@
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.VisualTree;
using OF_DL.Gui.ViewModels;
namespace OF_DL.Gui.Controls;
public class FileNameFormatOverlayTextBlock : TextBlock
{
public static readonly StyledProperty<IEnumerable<FileNameFormatSegmentViewModel>?> SegmentsProperty =
AvaloniaProperty.Register<FileNameFormatOverlayTextBlock, IEnumerable<FileNameFormatSegmentViewModel>?>(nameof(Segments));
public static readonly StyledProperty<TextBox?> SourceTextBoxProperty =
AvaloniaProperty.Register<FileNameFormatOverlayTextBlock, TextBox?>(nameof(SourceTextBox));
private INotifyCollectionChanged? _segmentsCollection;
private TextBox? _attachedTextBox;
private ScrollViewer? _attachedScrollViewer;
static FileNameFormatOverlayTextBlock()
{
SegmentsProperty.Changed.AddClassHandler<FileNameFormatOverlayTextBlock>(OnSegmentsChanged);
SourceTextBoxProperty.Changed.AddClassHandler<FileNameFormatOverlayTextBlock>(OnSourceTextBoxChanged);
}
public IEnumerable<FileNameFormatSegmentViewModel>? Segments
{
get => GetValue(SegmentsProperty);
set => SetValue(SegmentsProperty, value);
}
public TextBox? SourceTextBox
{
get => GetValue(SourceTextBoxProperty);
set => SetValue(SourceTextBoxProperty, value);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
DetachSegmentsCollection();
AttachSegmentsCollection(Segments);
AttachSourceTextBox(SourceTextBox);
RebuildInlines();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
DetachSourceTextBox();
DetachSegmentsCollection();
base.OnDetachedFromVisualTree(e);
}
private static void OnSegmentsChanged(
FileNameFormatOverlayTextBlock sender,
AvaloniaPropertyChangedEventArgs e)
{
sender.DetachSegmentsCollection();
sender.AttachSegmentsCollection(e.NewValue as IEnumerable<FileNameFormatSegmentViewModel>);
sender.RebuildInlines();
}
private static void OnSourceTextBoxChanged(
FileNameFormatOverlayTextBlock sender,
AvaloniaPropertyChangedEventArgs e)
{
sender.AttachSourceTextBox(e.NewValue as TextBox);
}
private void AttachSegmentsCollection(IEnumerable<FileNameFormatSegmentViewModel>? segments)
{
_segmentsCollection = segments as INotifyCollectionChanged;
if (_segmentsCollection is not null)
{
_segmentsCollection.CollectionChanged += OnSegmentsCollectionChanged;
}
}
private void DetachSegmentsCollection()
{
if (_segmentsCollection is null)
{
return;
}
_segmentsCollection.CollectionChanged -= OnSegmentsCollectionChanged;
_segmentsCollection = null;
}
private void OnSegmentsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RebuildInlines();
}
private void AttachSourceTextBox(TextBox? textBox)
{
if (ReferenceEquals(_attachedTextBox, textBox))
{
UpdateOverlayOffset();
return;
}
DetachSourceTextBox();
_attachedTextBox = textBox;
if (_attachedTextBox is null)
{
UpdateOverlayOffset();
return;
}
_attachedTextBox.TemplateApplied += OnSourceTextBoxTemplateApplied;
AttachScrollViewer(FindSourceScrollViewer(_attachedTextBox));
UpdateOverlayOffset();
}
private void DetachSourceTextBox()
{
if (_attachedTextBox is not null)
{
_attachedTextBox.TemplateApplied -= OnSourceTextBoxTemplateApplied;
}
if (_attachedScrollViewer is not null)
{
_attachedScrollViewer.ScrollChanged -= OnSourceScrollChanged;
_attachedScrollViewer = null;
}
_attachedTextBox = null;
UpdateOverlayOffset();
}
private void OnSourceTextBoxTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{
ScrollViewer? scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
AttachScrollViewer(scrollViewer ?? FindSourceScrollViewer(_attachedTextBox));
UpdateOverlayOffset();
}
private void OnSourceScrollChanged(object? sender, ScrollChangedEventArgs e)
{
UpdateOverlayOffset();
}
private void AttachScrollViewer(ScrollViewer? scrollViewer)
{
if (ReferenceEquals(_attachedScrollViewer, scrollViewer))
{
return;
}
if (_attachedScrollViewer is not null)
{
_attachedScrollViewer.ScrollChanged -= OnSourceScrollChanged;
}
_attachedScrollViewer = scrollViewer;
if (_attachedScrollViewer is not null)
{
_attachedScrollViewer.ScrollChanged += OnSourceScrollChanged;
}
}
private void UpdateOverlayOffset()
{
if (_attachedScrollViewer is null)
{
RenderTransform = null;
return;
}
RenderTransform = new TranslateTransform(-_attachedScrollViewer.Offset.X, -_attachedScrollViewer.Offset.Y);
}
private static ScrollViewer? FindSourceScrollViewer(TextBox? textBox)
{
if (textBox is null)
{
return null;
}
return textBox.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
}
private void RebuildInlines()
{
if (Inlines is null)
{
return;
}
Inlines.Clear();
if (Segments is null)
{
return;
}
foreach (FileNameFormatSegmentViewModel segment in Segments)
{
Run run = new()
{
Text = segment.Text ?? string.Empty,
Foreground = ParseForegroundBrush(segment.Foreground)
};
Inlines.Add(run);
}
}
private IBrush ParseForegroundBrush(string? value)
{
if (!string.IsNullOrWhiteSpace(value) && Color.TryParse(value, out Color color))
{
return new SolidColorBrush(color);
}
return Foreground ?? Brushes.Transparent;
}
}

View File

@ -140,7 +140,8 @@ public partial class ConfigFieldViewModel : ViewModelBase
public bool IsTimeoutField => public bool IsTimeoutField =>
string.Equals(PropertyName, nameof(Config.Timeout), StringComparison.Ordinal); string.Equals(PropertyName, nameof(Config.Timeout), StringComparison.Ordinal);
public bool IsRegularTextInput => IsTextInput && !IsIgnoredUsersListField && !IsCreatorConfigsField; public bool IsRegularTextInput =>
IsTextInput && !IsIgnoredUsersListField && !IsCreatorConfigsField && !IsFileNameFormatField;
public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText); public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText);
@ -167,6 +168,8 @@ public partial class ConfigFieldViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private string _textValue = string.Empty; private string _textValue = string.Empty;
private bool _isNormalizingFileNameFormatInput;
[ObservableProperty] [ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(InsertSelectedFileNameVariableCommand))] [NotifyCanExecuteChangedFor(nameof(InsertSelectedFileNameVariableCommand))]
private string? _selectedFileNameVariable; private string? _selectedFileNameVariable;
@ -364,6 +367,18 @@ public partial class ConfigFieldViewModel : ViewModelBase
partial void OnTextValueChanged(string value) partial void OnTextValueChanged(string value)
{ {
if (IsFileNameFormatField && !_isNormalizingFileNameFormatInput)
{
string trimmedValue = value.Trim();
if (!string.Equals(value, trimmedValue, StringComparison.Ordinal))
{
_isNormalizingFileNameFormatInput = true;
TextValue = trimmedValue;
_isNormalizingFileNameFormatInput = false;
return;
}
}
// Store actual value if not the privacy placeholder // Store actual value if not the privacy placeholder
if (value != "[Hidden for Privacy]") if (value != "[Hidden for Privacy]")
{ {

View File

@ -18,12 +18,16 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
private readonly Action<bool> _onClose; private readonly Action<bool> _onClose;
private readonly Func<bool> _isUsernameDuplicate; private readonly Func<bool> _isUsernameDuplicate;
private bool _isNormalizingUsername;
private bool _isNormalizingFileNameFormat;
[ObservableProperty] private bool _isOpen; [ObservableProperty] private bool _isOpen;
[ObservableProperty] private bool _isEditMode; [ObservableProperty] private bool _isEditMode;
[ObservableProperty] private string _originalUsername = string.Empty; [ObservableProperty] private string _originalUsername = string.Empty;
[ObservableProperty] private string _username = string.Empty; [ObservableProperty] private string _username = string.Empty;
[ObservableProperty] private string _usernameError = string.Empty; [ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasUsernameError))]
private string _usernameError = string.Empty;
[ObservableProperty] private string _paidPostFileNameFormat = string.Empty; [ObservableProperty] private string _paidPostFileNameFormat = string.Empty;
[ObservableProperty] private string _postFileNameFormat = string.Empty; [ObservableProperty] private string _postFileNameFormat = string.Empty;
[ObservableProperty] private string _paidMessageFileNameFormat = string.Empty; [ObservableProperty] private string _paidMessageFileNameFormat = string.Empty;
@ -32,10 +36,37 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
[ObservableProperty] private string _selectedPostVariable = string.Empty; [ObservableProperty] private string _selectedPostVariable = string.Empty;
[ObservableProperty] private string _selectedPaidMessageVariable = string.Empty; [ObservableProperty] private string _selectedPaidMessageVariable = string.Empty;
[ObservableProperty] private string _selectedMessageVariable = string.Empty; [ObservableProperty] private string _selectedMessageVariable = string.Empty;
[ObservableProperty] private string _unknownPaidPostVariablesMessage = string.Empty; [ObservableProperty]
[ObservableProperty] private string _unknownPostVariablesMessage = string.Empty; [NotifyPropertyChangedFor(nameof(HasUnknownPaidPostVariables))]
[ObservableProperty] private string _unknownPaidMessageVariablesMessage = string.Empty; private string _unknownPaidPostVariablesMessage = string.Empty;
[ObservableProperty] private string _unknownMessageVariablesMessage = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasUnknownPostVariables))]
private string _unknownPostVariablesMessage = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasUnknownPaidMessageVariables))]
private string _unknownPaidMessageVariablesMessage = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasUnknownMessageVariables))]
private string _unknownMessageVariablesMessage = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasPaidPostFileNameFormatError))]
private string _paidPostFileNameFormatError = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasPostFileNameFormatError))]
private string _postFileNameFormatError = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasPaidMessageFileNameFormatError))]
private string _paidMessageFileNameFormatError = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasMessageFileNameFormatError))]
private string _messageFileNameFormatError = string.Empty;
public ObservableCollection<string> AvailableUsers { get; } public ObservableCollection<string> AvailableUsers { get; }
public ObservableCollection<string> PaidPostVariables { get; } = []; public ObservableCollection<string> PaidPostVariables { get; } = [];
@ -52,6 +83,10 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
public bool HasUnknownPostVariables => !string.IsNullOrWhiteSpace(UnknownPostVariablesMessage); public bool HasUnknownPostVariables => !string.IsNullOrWhiteSpace(UnknownPostVariablesMessage);
public bool HasUnknownPaidMessageVariables => !string.IsNullOrWhiteSpace(UnknownPaidMessageVariablesMessage); public bool HasUnknownPaidMessageVariables => !string.IsNullOrWhiteSpace(UnknownPaidMessageVariablesMessage);
public bool HasUnknownMessageVariables => !string.IsNullOrWhiteSpace(UnknownMessageVariablesMessage); public bool HasUnknownMessageVariables => !string.IsNullOrWhiteSpace(UnknownMessageVariablesMessage);
public bool HasPaidPostFileNameFormatError => !string.IsNullOrWhiteSpace(PaidPostFileNameFormatError);
public bool HasPostFileNameFormatError => !string.IsNullOrWhiteSpace(PostFileNameFormatError);
public bool HasPaidMessageFileNameFormatError => !string.IsNullOrWhiteSpace(PaidMessageFileNameFormatError);
public bool HasMessageFileNameFormatError => !string.IsNullOrWhiteSpace(MessageFileNameFormatError);
public string DialogTitle => IsEditMode ? "Edit Creator Config" : "Add Creator Config"; public string DialogTitle => IsEditMode ? "Edit Creator Config" : "Add Creator Config";
@ -85,11 +120,11 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
IsEditMode = false; IsEditMode = false;
OriginalUsername = string.Empty; OriginalUsername = string.Empty;
Username = string.Empty; Username = string.Empty;
UsernameError = string.Empty;
PaidPostFileNameFormat = string.Empty; PaidPostFileNameFormat = string.Empty;
PostFileNameFormat = string.Empty; PostFileNameFormat = string.Empty;
PaidMessageFileNameFormat = string.Empty; PaidMessageFileNameFormat = string.Empty;
MessageFileNameFormat = string.Empty; MessageFileNameFormat = string.Empty;
ClearValidationErrors();
ClearAllPreviews(); ClearAllPreviews();
Log.Information("About to set IsOpen = true"); Log.Information("About to set IsOpen = true");
IsOpen = true; IsOpen = true;
@ -101,11 +136,11 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
IsEditMode = true; IsEditMode = true;
OriginalUsername = username; OriginalUsername = username;
Username = username; Username = username;
UsernameError = string.Empty;
PaidPostFileNameFormat = config.PaidPostFileNameFormat ?? string.Empty; PaidPostFileNameFormat = config.PaidPostFileNameFormat ?? string.Empty;
PostFileNameFormat = config.PostFileNameFormat ?? string.Empty; PostFileNameFormat = config.PostFileNameFormat ?? string.Empty;
PaidMessageFileNameFormat = config.PaidMessageFileNameFormat ?? string.Empty; PaidMessageFileNameFormat = config.PaidMessageFileNameFormat ?? string.Empty;
MessageFileNameFormat = config.MessageFileNameFormat ?? string.Empty; MessageFileNameFormat = config.MessageFileNameFormat ?? string.Empty;
ClearValidationErrors();
UpdateAllPreviews(); UpdateAllPreviews();
IsOpen = true; IsOpen = true;
} }
@ -184,32 +219,148 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
Log.Information("*** IsOpen property changed to: {Value} ***", value); Log.Information("*** IsOpen property changed to: {Value} ***", value);
} }
partial void OnPaidPostFileNameFormatChanged(string value) => UpdatePaidPostPreview(); partial void OnUsernameChanged(string value)
partial void OnPostFileNameFormatChanged(string value) => UpdatePostPreview(); {
partial void OnPaidMessageFileNameFormatChanged(string value) => UpdatePaidMessagePreview(); if (_isNormalizingUsername)
partial void OnMessageFileNameFormatChanged(string value) => UpdateMessagePreview(); {
return;
}
string trimmed = value.Trim();
if (!string.Equals(value, trimmed, StringComparison.Ordinal))
{
_isNormalizingUsername = true;
Username = trimmed;
_isNormalizingUsername = false;
}
UsernameError = string.Empty;
}
partial void OnPaidPostFileNameFormatChanged(string value) =>
NormalizeFileNameFormat(
value,
trimmed => PaidPostFileNameFormat = trimmed,
() => PaidPostFileNameFormatError = string.Empty,
UpdatePaidPostPreview);
partial void OnPostFileNameFormatChanged(string value) =>
NormalizeFileNameFormat(
value,
trimmed => PostFileNameFormat = trimmed,
() => PostFileNameFormatError = string.Empty,
UpdatePostPreview);
partial void OnPaidMessageFileNameFormatChanged(string value) =>
NormalizeFileNameFormat(
value,
trimmed => PaidMessageFileNameFormat = trimmed,
() => PaidMessageFileNameFormatError = string.Empty,
UpdatePaidMessagePreview);
partial void OnMessageFileNameFormatChanged(string value) =>
NormalizeFileNameFormat(
value,
trimmed => MessageFileNameFormat = trimmed,
() => MessageFileNameFormatError = string.Empty,
UpdateMessagePreview);
private bool Validate() private bool Validate()
{ {
UsernameError = string.Empty; ClearValidationErrors();
TrimInputValues();
bool isValid = true;
if (string.IsNullOrWhiteSpace(Username)) if (string.IsNullOrWhiteSpace(Username))
{ {
UsernameError = "Username is required."; UsernameError = "Username is required.";
return false; isValid = false;
} }
string trimmedUsername = Username.Trim(); string trimmedUsername = Username.Trim();
if (!IsEditMode || trimmedUsername != OriginalUsername) if (isValid && (!IsEditMode || trimmedUsername != OriginalUsername))
{ {
if (_isUsernameDuplicate()) if (_isUsernameDuplicate())
{ {
UsernameError = "A config for this username already exists."; UsernameError = "A config for this username already exists.";
return false; isValid = false;
} }
} }
return true; ValidateFileNameFormatUniqueness(PaidPostFileNameFormat, message => PaidPostFileNameFormatError = message,
ref isValid);
ValidateFileNameFormatUniqueness(PostFileNameFormat, message => PostFileNameFormatError = message,
ref isValid);
ValidateFileNameFormatUniqueness(PaidMessageFileNameFormat, message => PaidMessageFileNameFormatError = message,
ref isValid);
ValidateFileNameFormatUniqueness(MessageFileNameFormat, message => MessageFileNameFormatError = message,
ref isValid);
return isValid;
}
private void ClearValidationErrors()
{
UsernameError = string.Empty;
PaidPostFileNameFormatError = string.Empty;
PostFileNameFormatError = string.Empty;
PaidMessageFileNameFormatError = string.Empty;
MessageFileNameFormatError = string.Empty;
}
private void TrimInputValues()
{
Username = Username.Trim();
PaidPostFileNameFormat = PaidPostFileNameFormat.Trim();
PostFileNameFormat = PostFileNameFormat.Trim();
PaidMessageFileNameFormat = PaidMessageFileNameFormat.Trim();
MessageFileNameFormat = MessageFileNameFormat.Trim();
}
private static void ValidateFileNameFormatUniqueness(
string format,
Action<string> setError,
ref bool isValid)
{
if (string.IsNullOrWhiteSpace(format))
{
setError(string.Empty);
return;
}
bool hasUniqueToken = format.Contains("{mediaId}", StringComparison.OrdinalIgnoreCase) ||
format.Contains("{filename}", StringComparison.OrdinalIgnoreCase);
if (hasUniqueToken)
{
setError(string.Empty);
return;
}
setError("Format must include {mediaId} or {filename} to avoid file collisions.");
isValid = false;
}
private void NormalizeFileNameFormat(
string value,
Action<string> setValue,
Action clearError,
Action updatePreview)
{
if (!_isNormalizingFileNameFormat)
{
string trimmedValue = value.Trim();
if (!string.Equals(value, trimmedValue, StringComparison.Ordinal))
{
_isNormalizingFileNameFormat = true;
setValue(trimmedValue);
_isNormalizingFileNameFormat = false;
return;
}
}
clearError();
updatePreview();
} }
private void ClearAllPreviews() private void ClearAllPreviews()

View File

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:OF_DL.Gui.Controls"
xmlns:vm="using:OF_DL.Gui.ViewModels" xmlns:vm="using:OF_DL.Gui.ViewModels"
xmlns:views="using:OF_DL.Gui.Views" xmlns:views="using:OF_DL.Gui.Views"
x:Class="OF_DL.Gui.Views.MainWindow" x:Class="OF_DL.Gui.Views.MainWindow"
@ -38,6 +39,10 @@
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="FontWeight" Value="SemiBold" />
</Style> </Style>
<Style Selector="TextBox.fileNameOverlayInput:not(:empty)">
<Setter Property="Foreground" Value="Transparent" />
</Style>
</Window.Styles> </Window.Styles>
<Grid RowDefinitions="Auto,Auto,*"> <Grid RowDefinitions="Auto,Auto,*">
@ -646,6 +651,26 @@
<StackPanel <StackPanel
IsVisible="{Binding IsFileNameFormatField}" IsVisible="{Binding IsFileNameFormatField}"
Spacing="6"> Spacing="6">
<Grid ClipToBounds="True">
<TextBox
x:Name="ConfigFileNameFormatTextBox"
Classes="fileNameOverlayInput"
HorizontalAlignment="Stretch"
Text="{Binding TextValue}"
Padding="8,6"
CaretBrush="{DynamicResource TextPrimaryBrush}" />
<controls:FileNameFormatOverlayTextBlock
IsHitTestVisible="False"
Margin="{Binding #ConfigFileNameFormatTextBox.Padding}"
VerticalAlignment="Center"
Segments="{Binding FileNameFormatSegments}"
SourceTextBox="{Binding #ConfigFileNameFormatTextBox}"
FontFamily="{Binding #ConfigFileNameFormatTextBox.FontFamily}"
FontSize="{Binding #ConfigFileNameFormatTextBox.FontSize}"
FontWeight="{Binding #ConfigFileNameFormatTextBox.FontWeight}"
FontStyle="{Binding #ConfigFileNameFormatTextBox.FontStyle}" />
</Grid>
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<ComboBox Grid.Column="0" <ComboBox Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
@ -658,32 +683,6 @@
Command="{Binding InsertSelectedFileNameVariableCommand}" /> Command="{Binding InsertSelectedFileNameVariableCommand}" />
</Grid> </Grid>
<Border Padding="8"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
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 <TextBlock
IsVisible="{Binding HasUnknownFileNameVariables}" IsVisible="{Binding HasUnknownFileNameVariables}"
Foreground="{DynamicResource ErrorTextBrush}" Foreground="{DynamicResource ErrorTextBrush}"
@ -992,8 +991,23 @@
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Paid Post Filename Format" /> <TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Paid Post Filename Format" />
<TextBox Text="{Binding PaidPostFileNameFormat}" <Grid ClipToBounds="True">
Watermark="Optional: override global paid post format" /> <TextBox x:Name="PaidPostFileNameFormatTextBox"
Classes="fileNameOverlayInput"
Text="{Binding PaidPostFileNameFormat}"
Watermark="Optional: override global paid post format"
Padding="8,6"
CaretBrush="{DynamicResource TextPrimaryBrush}" />
<controls:FileNameFormatOverlayTextBlock IsHitTestVisible="False"
Margin="{Binding #PaidPostFileNameFormatTextBox.Padding}"
VerticalAlignment="Center"
Segments="{Binding PaidPostSegments}"
SourceTextBox="{Binding #PaidPostFileNameFormatTextBox}"
FontFamily="{Binding #PaidPostFileNameFormatTextBox.FontFamily}"
FontSize="{Binding #PaidPostFileNameFormatTextBox.FontSize}"
FontWeight="{Binding #PaidPostFileNameFormatTextBox.FontWeight}"
FontStyle="{Binding #PaidPostFileNameFormatTextBox.FontStyle}" />
</Grid>
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<ComboBox Grid.Column="0" <ComboBox Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
@ -1005,37 +1019,35 @@
Content="Insert" Content="Insert"
Command="{Binding InsertPaidPostVariableCommand}" /> Command="{Binding InsertPaidPostVariableCommand}" />
</Grid> </Grid>
<Border Padding="8"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<ItemsControl ItemsSource="{Binding PaidPostSegments}">
<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 HasUnknownPaidPostVariables}" <TextBlock IsVisible="{Binding HasUnknownPaidPostVariables}"
Foreground="{DynamicResource ErrorTextBrush}" Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UnknownPaidPostVariablesMessage}" Text="{Binding UnknownPaidPostVariablesMessage}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock IsVisible="{Binding HasPaidPostFileNameFormatError}"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding PaidPostFileNameFormatError}"
TextWrapping="Wrap" />
</StackPanel> </StackPanel>
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Post Filename Format" /> <TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Post Filename Format" />
<TextBox Text="{Binding PostFileNameFormat}" <Grid ClipToBounds="True">
Watermark="Optional: override global post format" /> <TextBox x:Name="PostFileNameFormatTextBox"
Classes="fileNameOverlayInput"
Text="{Binding PostFileNameFormat}"
Watermark="Optional: override global post format"
Padding="8,6"
CaretBrush="{DynamicResource TextPrimaryBrush}" />
<controls:FileNameFormatOverlayTextBlock IsHitTestVisible="False"
Margin="{Binding #PostFileNameFormatTextBox.Padding}"
VerticalAlignment="Center"
Segments="{Binding PostSegments}"
SourceTextBox="{Binding #PostFileNameFormatTextBox}"
FontFamily="{Binding #PostFileNameFormatTextBox.FontFamily}"
FontSize="{Binding #PostFileNameFormatTextBox.FontSize}"
FontWeight="{Binding #PostFileNameFormatTextBox.FontWeight}"
FontStyle="{Binding #PostFileNameFormatTextBox.FontStyle}" />
</Grid>
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<ComboBox Grid.Column="0" <ComboBox Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
@ -1047,37 +1059,35 @@
Content="Insert" Content="Insert"
Command="{Binding InsertPostVariableCommand}" /> Command="{Binding InsertPostVariableCommand}" />
</Grid> </Grid>
<Border Padding="8"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<ItemsControl ItemsSource="{Binding PostSegments}">
<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 HasUnknownPostVariables}" <TextBlock IsVisible="{Binding HasUnknownPostVariables}"
Foreground="{DynamicResource ErrorTextBrush}" Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UnknownPostVariablesMessage}" Text="{Binding UnknownPostVariablesMessage}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock IsVisible="{Binding HasPostFileNameFormatError}"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding PostFileNameFormatError}"
TextWrapping="Wrap" />
</StackPanel> </StackPanel>
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Paid Message Filename Format" /> <TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Paid Message Filename Format" />
<TextBox Text="{Binding PaidMessageFileNameFormat}" <Grid ClipToBounds="True">
Watermark="Optional: override global paid message format" /> <TextBox x:Name="PaidMessageFileNameFormatTextBox"
Classes="fileNameOverlayInput"
Text="{Binding PaidMessageFileNameFormat}"
Watermark="Optional: override global paid message format"
Padding="8,6"
CaretBrush="{DynamicResource TextPrimaryBrush}" />
<controls:FileNameFormatOverlayTextBlock IsHitTestVisible="False"
Margin="{Binding #PaidMessageFileNameFormatTextBox.Padding}"
VerticalAlignment="Center"
Segments="{Binding PaidMessageSegments}"
SourceTextBox="{Binding #PaidMessageFileNameFormatTextBox}"
FontFamily="{Binding #PaidMessageFileNameFormatTextBox.FontFamily}"
FontSize="{Binding #PaidMessageFileNameFormatTextBox.FontSize}"
FontWeight="{Binding #PaidMessageFileNameFormatTextBox.FontWeight}"
FontStyle="{Binding #PaidMessageFileNameFormatTextBox.FontStyle}" />
</Grid>
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<ComboBox Grid.Column="0" <ComboBox Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
@ -1089,37 +1099,35 @@
Content="Insert" Content="Insert"
Command="{Binding InsertPaidMessageVariableCommand}" /> Command="{Binding InsertPaidMessageVariableCommand}" />
</Grid> </Grid>
<Border Padding="8"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<ItemsControl ItemsSource="{Binding PaidMessageSegments}">
<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 HasUnknownPaidMessageVariables}" <TextBlock IsVisible="{Binding HasUnknownPaidMessageVariables}"
Foreground="{DynamicResource ErrorTextBrush}" Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UnknownPaidMessageVariablesMessage}" Text="{Binding UnknownPaidMessageVariablesMessage}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock IsVisible="{Binding HasPaidMessageFileNameFormatError}"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding PaidMessageFileNameFormatError}"
TextWrapping="Wrap" />
</StackPanel> </StackPanel>
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Message Filename Format" /> <TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Message Filename Format" />
<TextBox Text="{Binding MessageFileNameFormat}" <Grid ClipToBounds="True">
Watermark="Optional: override global message format" /> <TextBox x:Name="MessageFileNameFormatTextBox"
Classes="fileNameOverlayInput"
Text="{Binding MessageFileNameFormat}"
Watermark="Optional: override global message format"
Padding="8,6"
CaretBrush="{DynamicResource TextPrimaryBrush}" />
<controls:FileNameFormatOverlayTextBlock IsHitTestVisible="False"
Margin="{Binding #MessageFileNameFormatTextBox.Padding}"
VerticalAlignment="Center"
Segments="{Binding MessageSegments}"
SourceTextBox="{Binding #MessageFileNameFormatTextBox}"
FontFamily="{Binding #MessageFileNameFormatTextBox.FontFamily}"
FontSize="{Binding #MessageFileNameFormatTextBox.FontSize}"
FontWeight="{Binding #MessageFileNameFormatTextBox.FontWeight}"
FontStyle="{Binding #MessageFileNameFormatTextBox.FontStyle}" />
</Grid>
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<ComboBox Grid.Column="0" <ComboBox Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
@ -1131,31 +1139,14 @@
Content="Insert" Content="Insert"
Command="{Binding InsertMessageVariableCommand}" /> Command="{Binding InsertMessageVariableCommand}" />
</Grid> </Grid>
<Border Padding="8"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<ItemsControl ItemsSource="{Binding MessageSegments}">
<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 HasUnknownMessageVariables}" <TextBlock IsVisible="{Binding HasUnknownMessageVariables}"
Foreground="{DynamicResource ErrorTextBrush}" Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UnknownMessageVariablesMessage}" Text="{Binding UnknownMessageVariablesMessage}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock IsVisible="{Binding HasMessageFileNameFormatError}"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding MessageFileNameFormatError}"
TextWrapping="Wrap" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right"> <StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">