Use new Inline controls to selectively style TextBlock text
This commit is contained in:
parent
fe804796ab
commit
acb6d1b335
@ -8,11 +8,6 @@
|
|||||||
Icon="/Assets/libation.ico"
|
Icon="/Assets/libation.ico"
|
||||||
Title="EditTemplateDialog">
|
Title="EditTemplateDialog">
|
||||||
|
|
||||||
<Window.Resources>
|
|
||||||
<dialogs:BracketEscapeConverter x:Key="BracketEscapeConverter" />
|
|
||||||
</Window.Resources>
|
|
||||||
|
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,*,Auto">
|
<Grid RowDefinitions="Auto,*,Auto">
|
||||||
<Grid
|
<Grid
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@ -27,37 +22,33 @@
|
|||||||
<TextBox
|
<TextBox
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Text="{Binding workingTemplateText, Mode=TwoWay}" />
|
Text="{Binding UserTemplateText, Mode=TwoWay}" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Margin="10,0,0,0"
|
Margin="10,0,0,0"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
Padding="20,3,20,3"
|
VerticalContentAlignment="Center"
|
||||||
Content="Reset to Default"
|
Content="Reset to Default"
|
||||||
Click="ResetButton_Click" />
|
Click="ResetButton_Click" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
|
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
|
||||||
|
|
||||||
<Border
|
<DataGrid
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="5"
|
BorderBrush="{DynamicResource DataGridGridLinesBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
|
||||||
|
|
||||||
<DataGrid
|
|
||||||
GridLinesVisibility="All"
|
GridLinesVisibility="All"
|
||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
Items="{Binding ListItems}" >
|
Items="{Binding ListItems}" >
|
||||||
|
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
|
|
||||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding TagName, Converter={StaticResource BracketEscapeConverter}}" />
|
<TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
@ -68,7 +59,7 @@
|
|||||||
<TextPresenter
|
<TextPresenter
|
||||||
Height="18"
|
Height="18"
|
||||||
Margin="10,0,10,0"
|
Margin="10,0,10,0"
|
||||||
VerticalAlignment="Center" Text="{Binding Description}" />
|
VerticalAlignment="Center" Text="{Binding Item2}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
@ -76,13 +67,12 @@
|
|||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="5"
|
Margin="5,0,5,0"
|
||||||
RowDefinitions="Auto,*,80" HorizontalAlignment="Stretch">
|
RowDefinitions="Auto,*,Auto"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="5,5,5,10"
|
Margin="5,5,5,10"
|
||||||
@ -94,10 +84,9 @@
|
|||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
||||||
|
|
||||||
<WrapPanel
|
<TextBlock
|
||||||
Grid.Row="1"
|
TextWrapping="WrapWithOverflow"
|
||||||
Name="wrapPanel"
|
Inlines="{Binding Inlines}" />
|
||||||
Orientation="Horizontal" />
|
|
||||||
|
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@ -105,10 +94,9 @@
|
|||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Foreground="Firebrick"
|
Foreground="Firebrick"
|
||||||
Text="{Binding WarningText}" />
|
Text="{Binding WarningText}"
|
||||||
|
IsVisible="{Binding WarningText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Button
|
<Button
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Media.TextFormatting;
|
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System;
|
using System;
|
||||||
@ -14,6 +11,9 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using Avalonia.Controls.Documents;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
namespace LibationAvalonia.Dialogs
|
namespace LibationAvalonia.Dialogs
|
||||||
{
|
{
|
||||||
@ -22,14 +22,14 @@ namespace LibationAvalonia.Dialogs
|
|||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
if (value is string str && str[0] != '<' && str[^1] != '>')
|
if (value is string str && str[0] != '<' && str[^1] != '>')
|
||||||
return $"<{str}>";
|
return $"<{str}>".Replace("->", "-\x200C>").Replace("<-", "<\x200C-");
|
||||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
if (value is string str && str[0] == '<' && str[^1] == '>')
|
if (value is string str && str[0] == '<' && str[^1] == '>')
|
||||||
return str[1..^2];
|
return str[1..^2].Replace("-\x200C>", "->").Replace("<\x200C-", "<-");
|
||||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,26 +42,27 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
public EditTemplateDialog()
|
public EditTemplateDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
AvaloniaXamlLoader.Load(this);
|
||||||
_viewModel = new(Configuration.Instance, this.Find<WrapPanel>(nameof(wrapPanel)));
|
if (Design.IsDesignMode)
|
||||||
|
{
|
||||||
|
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||||
|
_viewModel = new(Configuration.Instance, Templates.File);
|
||||||
|
_viewModel.resetTextBox(_viewModel.Template.DefaultTemplate);
|
||||||
|
Title = $"Edit {_viewModel.Template.Name}";
|
||||||
|
DataContext = _viewModel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public EditTemplateDialog(Templates template, string inputTemplateText) : this()
|
public EditTemplateDialog(Templates template, string inputTemplateText) : this()
|
||||||
{
|
{
|
||||||
_viewModel.template = ArgumentValidator.EnsureNotNull(template, nameof(template));
|
ArgumentValidator.EnsureNotNull(template, nameof(template));
|
||||||
Title = $"Edit {_viewModel.template.Name}";
|
|
||||||
_viewModel.Description = _viewModel.template.Description;
|
_viewModel = new EditTemplateViewModel(Configuration.Instance, template);
|
||||||
_viewModel.resetTextBox(inputTemplateText);
|
_viewModel.resetTextBox(inputTemplateText);
|
||||||
|
Title = $"Edit {template.Name}";
|
||||||
_viewModel.ListItems = _viewModel.template.GetTemplateTags();
|
|
||||||
|
|
||||||
DataContext = _viewModel;
|
DataContext = _viewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
protected override async Task SaveAndCloseAsync()
|
protected override async Task SaveAndCloseAsync()
|
||||||
{
|
{
|
||||||
if (!await _viewModel.Validate())
|
if (!await _viewModel.Validate())
|
||||||
@ -75,51 +76,57 @@ namespace LibationAvalonia.Dialogs
|
|||||||
=> await SaveAndCloseAsync();
|
=> await SaveAndCloseAsync();
|
||||||
|
|
||||||
public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
=> _viewModel.resetTextBox(_viewModel.template.DefaultTemplate);
|
=> _viewModel.resetTextBox(_viewModel.Template.DefaultTemplate);
|
||||||
|
|
||||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||||
{
|
{
|
||||||
WrapPanel WrapPanel;
|
private readonly Configuration config;
|
||||||
public Configuration config { get; }
|
public InlineCollection Inlines { get; } = new();
|
||||||
public EditTemplateViewModel(Configuration configuration, WrapPanel panel)
|
public Templates Template { get; }
|
||||||
|
public EditTemplateViewModel(Configuration configuration, Templates templates)
|
||||||
{
|
{
|
||||||
config = configuration;
|
config = configuration;
|
||||||
WrapPanel = panel;
|
Template = templates;
|
||||||
|
Description = templates.Description;
|
||||||
|
ListItems
|
||||||
|
= new AvaloniaList<Tuple<string, string>>(
|
||||||
|
Template
|
||||||
|
.GetTemplateTags()
|
||||||
|
.Select(
|
||||||
|
t => new Tuple<string, string>(
|
||||||
|
$"<{t.TagName.Replace("->", "-\x200C>").Replace("<-", "<\x200C-")}>",
|
||||||
|
t.Description)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// hold the work-in-progress value. not guaranteed to be valid
|
// hold the work-in-progress value. not guaranteed to be valid
|
||||||
private string _workingTemplateText;
|
private string _userTemplateText;
|
||||||
public string workingTemplateText
|
public string UserTemplateText
|
||||||
{
|
{
|
||||||
get => _workingTemplateText;
|
get => _userTemplateText;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_workingTemplateText = template.Sanitize(value);
|
_userTemplateText = value;
|
||||||
templateTb_TextChanged();
|
templateTb_TextChanged();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string workingTemplateText => Template.Sanitize(UserTemplateText);
|
||||||
private string _warningText;
|
private string _warningText;
|
||||||
public string WarningText
|
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||||
{
|
|
||||||
get => _warningText;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
this.RaiseAndSetIfChanged(ref _warningText, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Templates template { get; set; }
|
public string Description { get; }
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
public IEnumerable<TemplateTags> ListItems { get; set; }
|
public AvaloniaList<Tuple<string, string>> ListItems { get; set; }
|
||||||
|
|
||||||
public void resetTextBox(string value) => workingTemplateText = value;
|
public void resetTextBox(string value) => UserTemplateText = value;
|
||||||
|
|
||||||
public async Task<bool> Validate()
|
public async Task<bool> Validate()
|
||||||
{
|
{
|
||||||
if (template.IsValid(workingTemplateText))
|
if (Template.IsValid(workingTemplateText))
|
||||||
return true;
|
return true;
|
||||||
var errors = template
|
var errors = Template
|
||||||
.GetErrors(workingTemplateText)
|
.GetErrors(workingTemplateText)
|
||||||
.Select(err => $"- {err}")
|
.Select(err => $"- {err}")
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
@ -129,8 +136,8 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
private void templateTb_TextChanged()
|
private void templateTb_TextChanged()
|
||||||
{
|
{
|
||||||
var isChapterTitle = template == Templates.ChapterTitle;
|
var isChapterTitle = Template == Templates.ChapterTitle;
|
||||||
var isFolder = template == Templates.Folder;
|
var isFolder = Template == Templates.Folder;
|
||||||
|
|
||||||
var libraryBookDto = new LibraryBookDto
|
var libraryBookDto = new LibraryBookDto
|
||||||
{
|
{
|
||||||
@ -142,7 +149,10 @@ namespace LibationAvalonia.Dialogs
|
|||||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||||
Narrators = new List<string> { "Stephen Fry" },
|
Narrators = new List<string> { "Stephen Fry" },
|
||||||
SeriesName = "Sherlock Holmes",
|
SeriesName = "Sherlock Holmes",
|
||||||
SeriesNumber = "1"
|
SeriesNumber = "1",
|
||||||
|
BitRate = 128,
|
||||||
|
SampleRate = 44100,
|
||||||
|
Channels = 2
|
||||||
};
|
};
|
||||||
var chapterName = "A Flight for Life";
|
var chapterName = "A Flight for Life";
|
||||||
var chapterNumber = 4;
|
var chapterNumber = 4;
|
||||||
@ -162,15 +172,15 @@ namespace LibationAvalonia.Dialogs
|
|||||||
isFolder ? workingTemplateText : config.FolderTemplate);
|
isFolder ? workingTemplateText : config.FolderTemplate);
|
||||||
|
|
||||||
var file
|
var file
|
||||||
= template == Templates.ChapterFile
|
= Template == Templates.ChapterFile
|
||||||
? Templates.ChapterFile.GetPortionFilename(
|
? Templates.ChapterFile.GetPortionFilename(
|
||||||
libraryBookDto,
|
libraryBookDto,
|
||||||
workingTemplateText,
|
workingTemplateText,
|
||||||
partFileProperties,
|
partFileProperties,
|
||||||
"")
|
"")
|
||||||
: Templates.File.GetPortionFilename(
|
: Templates.File.GetPortionFilename(
|
||||||
libraryBookDto,
|
libraryBookDto,
|
||||||
isFolder ? config.FileTemplate : workingTemplateText);
|
isFolder ? config.FileTemplate : workingTemplateText);
|
||||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||||
|
|
||||||
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);
|
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);
|
||||||
@ -186,63 +196,36 @@ namespace LibationAvalonia.Dialogs
|
|||||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||||
|
|
||||||
WarningText
|
WarningText
|
||||||
= !template.HasWarnings(workingTemplateText)
|
= !Template.HasWarnings(workingTemplateText)
|
||||||
? ""
|
? ""
|
||||||
: "Warning:\r\n" +
|
: "Warning:\r\n" +
|
||||||
template
|
Template
|
||||||
.GetWarnings(workingTemplateText)
|
.GetWarnings(workingTemplateText)
|
||||||
.Select(err => $"- {err}")
|
.Select(err => $"- {err}")
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
|
|
||||||
var list = new List<TextCharacters>();
|
var bold = FontWeight.Bold;
|
||||||
|
var reg = FontWeight.Normal;
|
||||||
|
|
||||||
var bold = new Typeface(Typeface.Default.FontFamily, FontStyle.Normal, FontWeight.Bold);
|
Inlines.Clear();
|
||||||
var normal = new Typeface(Typeface.Default.FontFamily, FontStyle.Normal, FontWeight.Normal);
|
|
||||||
|
|
||||||
var stringList = new List<(string, FontWeight)>();
|
|
||||||
|
|
||||||
if (isChapterTitle)
|
if (isChapterTitle)
|
||||||
{
|
{
|
||||||
stringList.Add((chapterTitle, FontWeight.Bold));
|
Inlines.Add(new Run(chapterTitle) { FontWeight = bold });
|
||||||
}
|
return;
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
stringList.Add((slashWrap(books), FontWeight.Normal));
|
|
||||||
stringList.Add((sing, FontWeight.Normal));
|
|
||||||
|
|
||||||
stringList.Add((slashWrap(folder), isFolder ? FontWeight.Bold : FontWeight.Normal));
|
|
||||||
|
|
||||||
stringList.Add((sing, FontWeight.Normal));
|
|
||||||
|
|
||||||
stringList.Add((file, !isFolder ? FontWeight.Bold : FontWeight.Normal));
|
|
||||||
|
|
||||||
stringList.Add(($".{ext}", FontWeight.Normal));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapPanel.Children.Clear();
|
Inlines.Add(new Run(slashWrap(books)) { FontWeight = reg });
|
||||||
|
Inlines.Add(new Run(sing) { FontWeight = reg });
|
||||||
|
|
||||||
//Avalonia doesn't yet support anything like rich text, so add a new textblock for every word/style
|
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = isFolder ? bold : reg });
|
||||||
foreach (var item in stringList)
|
|
||||||
{
|
|
||||||
var wordsSplit = item.Item1.Split(' ');
|
|
||||||
|
|
||||||
for(int i = 0; i < wordsSplit.Length; i++)
|
Inlines.Add(new Run(sing));
|
||||||
{
|
|
||||||
var tb = new TextBlock
|
|
||||||
{
|
|
||||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
Text = wordsSplit[i] + (i == wordsSplit.Length - 1 ? "" : " "),
|
|
||||||
FontWeight = item.Item2
|
|
||||||
};
|
|
||||||
|
|
||||||
WrapPanel.Children.Add(tb);
|
Inlines.Add(new Run(slashWrap(file)) { FontWeight = isFolder ? reg : bold });
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Inlines.Add(new Run($".{ext}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user