diff --git a/src/UniGetUI.Avalonia/App.axaml b/src/UniGetUI.Avalonia/App.axaml index e948a7b790..0090c8aa0b 100644 --- a/src/UniGetUI.Avalonia/App.axaml +++ b/src/UniGetUI.Avalonia/App.axaml @@ -11,6 +11,7 @@ + diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index 66cf6a063e..66a3bfa672 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -126,6 +126,7 @@ + diff --git a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs index 815d7d3cf3..0c20c6432e 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs @@ -60,7 +60,7 @@ partial void OnIsPaneOpenChanged(bool value) OnPropertyChanged(nameof(BundlesBadgeCompactVisible)); } - public double PaneWidth => IsPaneOpen ? 250 : 72; + public double PaneWidth => IsPaneOpen ? 250 : 64; public bool UpdatesBadgeExpandedVisible => UpdatesBadgeVisible && IsPaneOpen; public bool UpdatesBadgeCompactVisible => UpdatesBadgeVisible && !IsPaneOpen; diff --git a/src/UniGetUI.Avalonia/Views/Controls/LogTextEditor.cs b/src/UniGetUI.Avalonia/Views/Controls/LogTextEditor.cs new file mode 100644 index 0000000000..63791ae0aa --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Controls/LogTextEditor.cs @@ -0,0 +1,104 @@ +using System.Text; +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using Avalonia.Styling; +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Rendering; +using UniGetUI.Avalonia.ViewModels.Pages.LogPages; + +namespace UniGetUI.Avalonia.Views.Controls; + +// Read-only colored log/console viewer shared by every log and command-output view: virtualized +// rendering (smooth scroll), free character-level selection across lines, per-line colors, and a +// theme-aware hyperlink color. +public class LogTextEditor : TextEditor +{ + // Per physical line color, indexed by 0-based document line number. + private readonly List _lineColors = []; + + // Use AvaloniaEdit's TextEditor theme/template; a subclass key has no matching ControlTheme. + protected override Type StyleKeyOverride => typeof(TextEditor); + + public LogTextEditor() + { + IsReadOnly = true; + ShowLineNumbers = false; + WordWrap = true; + Background = Brushes.Transparent; + FontFamily = new FontFamily("Cascadia Mono,Consolas,Menlo,monospace"); + FontSize = 12; + Padding = new Thickness(8); + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + + TextArea.TextView.LineTransformers.Add(new SeverityColorizer(_lineColors)); + UpdateLinkColor(); + ActualThemeVariantChanged += (_, _) => UpdateLinkColor(); + } + + public void SetLines(IEnumerable lines) + { + _lineColors.Clear(); + var sb = new StringBuilder(); + bool first = true; + + foreach (var item in lines) + { + foreach (string physicalLine in item.Text.Split('\n')) + { + if (!first) sb.Append('\n'); + sb.Append(physicalLine); + _lineColors.Add(item.Foreground); + first = false; + } + } + + Text = sb.ToString(); + TextArea.TextView.Redraw(); + } + + public void AppendLine(LogLineItem line) + { + var sb = new StringBuilder(); + foreach (string physicalLine in line.Text.Split('\n')) + { + if (Document.TextLength > 0 || sb.Length > 0) + sb.Append('\n'); + sb.Append(physicalLine); + _lineColors.Add(line.Foreground); + } + + Document.Insert(Document.TextLength, sb.ToString()); + } + + public void ClearLines() + { + _lineColors.Clear(); + Text = string.Empty; + } + + public void ScrollToBottom() => ScrollToEnd(); + + // AvaloniaEdit auto-links URLs; its default link brush is too dark to read on the dark theme. + private void UpdateLinkColor() + { + bool isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark; + TextArea.TextView.LinkTextForegroundBrush = + new SolidColorBrush(isDark ? Color.FromRgb(100, 170, 255) : Color.FromRgb(0, 0, 205)); + } + + private sealed class SeverityColorizer(List lineColors) : DocumentColorizingTransformer + { + protected override void ColorizeLine(DocumentLine line) + { + if (line.Length == 0) return; + int index = line.LineNumber - 1; + if (index < 0 || index >= lineColors.Count) return; + + IBrush brush = lineColors[index]; + ChangeLinePart(line.Offset, line.EndOffset, element => element.TextRunProperties.SetForegroundBrush(brush)); + } + } +} diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsCard.cs index 4d28d527fe..4f1f7e48f4 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsCard.cs @@ -6,6 +6,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; +using UniGetUI.Avalonia.Views.Controls; using ICommand = System.Windows.Input.ICommand; namespace UniGetUI.Avalonia.Views.Controls.Settings; @@ -23,7 +24,7 @@ public class SettingsCard : UserControl private readonly ContentControl _descriptionPresenter; private readonly ContentControl _contentPresenter; private readonly StackPanel _descriptionRow; - private readonly TextBlock _chevron; + private readonly SvgIcon _chevron; // ── Styled properties ────────────────────────────────────────────────── public static readonly StyledProperty HeaderProperty = @@ -178,10 +179,11 @@ public SettingsCard() Margin = new Thickness(16, 0, 0, 0), }; - _chevron = new TextBlock + _chevron = new SvgIcon { - Text = "›", - FontSize = 20, + Path = "avares://UniGetUI.Avalonia/Assets/Symbols/forward.svg", + Width = 16, + Height = 16, VerticalAlignment = VerticalAlignment.Center, Opacity = 0.6, Margin = new Thickness(8, 0, 0, 0), diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml index 00cd0955e3..4d349edc9a 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml @@ -1,6 +1,7 @@ - - - + + + (); foreach (var (text, type) in operation.GetOutput()) { IBrush brush = type switch @@ -40,10 +39,9 @@ public OperationFailedDialog(AbstractOperation operation) AbstractOperation.LineType.VerboseDetails => debugBrush, _ => normalBrush, }; - if (!first) inlines.Add(new LineBreak()); - inlines.Add(new Run(text) { Foreground = brush }); - first = false; + lines.Add(new LogLineItem(text, brush)); } + OutputText.SetLines(lines); var closeButton = new Button { @@ -64,7 +62,7 @@ public OperationFailedDialog(AbstractOperation operation) protected override void OnOpened(EventArgs e) { base.OnOpened(e); - Dispatcher.UIThread.Post(OutputScroll.ScrollToEnd, DispatcherPriority.Background); + Dispatcher.UIThread.Post(OutputText.ScrollToBottom, DispatcherPriority.Background); } private Control BuildRetryButton(AbstractOperation operation) diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml index 4fc3e74435..e77bcc6c72 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:automation="clr-namespace:Avalonia.Automation;assembly=Avalonia.Controls" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.DialogPages" + xmlns:controls="using:UniGetUI.Avalonia.Views.Controls" x:Class="UniGetUI.Avalonia.Views.DialogPages.OperationOutputWindow" x:DataType="vm:OperationOutputViewModel" Width="700" MinWidth="400" @@ -12,19 +13,13 @@ WindowStartupLocation="CenterOwner" Title="{Binding Title}"> - - - + + + diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs index 92751bbcdd..50fb308f30 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs @@ -1,6 +1,5 @@ using System.Collections.Specialized; using Avalonia.Controls; -using Avalonia.Controls.Documents; using Avalonia.Threading; using UniGetUI.Avalonia.ViewModels.DialogPages; using UniGetUI.Avalonia.ViewModels.Pages.LogPages; @@ -17,9 +16,7 @@ public OperationOutputWindow(AbstractOperation operation) InitializeComponent(); UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); - foreach (var line in vm.OutputLines) - AppendLine(line); - + OutputText.SetLines(vm.OutputLines); vm.OutputLines.CollectionChanged += OnOutputLinesChanged; } @@ -29,28 +26,20 @@ private void OnOutputLinesChanged(object? sender, NotifyCollectionChangedEventAr { if (e.Action == NotifyCollectionChangedAction.Reset) { - OutputText.Inlines?.Clear(); + OutputText.ClearLines(); } else if (e.NewItems is not null) { foreach (LogLineItem item in e.NewItems) - AppendLine(item); + OutputText.AppendLine(item); } - OutputScroll.ScrollToEnd(); + OutputText.ScrollToBottom(); }, DispatcherPriority.Background); } - private void AppendLine(LogLineItem line) - { - var inlines = OutputText.Inlines ??= new InlineCollection(); - if (inlines.Count > 0) - inlines.Add(new LineBreak()); - inlines.Add(new Run(line.Text) { Foreground = line.Foreground }); - } - protected override void OnOpened(EventArgs e) { base.OnOpened(e); - OutputScroll.ScrollToEnd(); + OutputText.ScrollToBottom(); } } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml index 91db051f49..de84f78426 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml @@ -253,9 +253,11 @@ Spacing="8"> + + + + + + + + + + + + - + diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml index 8d58446cf2..dd056d2ce6 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:automation="clr-namespace:Avalonia.Automation;assembly=Avalonia.Controls" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages.LogPages" + xmlns:controls="using:UniGetUI.Avalonia.Views.Controls" xmlns:t="using:UniGetUI.Avalonia.MarkupExtensions" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -44,6 +45,7 @@ @@ -647,28 +650,33 @@ - - + + VerticalAlignment="Center" + MinHeight="0" + Padding="0" Margin="2,0,0,0"/> + + + + +