Draw Avalonia Liberate icons from the shared generator

Replace the hand-composed vector layers with the shared rendering bound through
EntryStatus.ButtonImage, and delete the geometry and stoplight brushes that only
that composition used.

The rendering is per-theme rather than a set of DynamicResource brushes, so each
button re-renders its icon when the actual theme variant changes.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
This commit is contained in:
Cursor Agentandrmcrackan committed 2026-08-13 05:56:29 +00:00
1 parent 7b0bd603a6
commit dcaa42d21d
7 files changed
+23 -153

No files matched your search

-6
View File
@@ -31,9 +31,6 @@
<SolidColorBrush x:Key="HyperlinkVisited" Color="Purple" />
<SolidColorBrush x:Key="CancelRed" Color="FireBrick" />
<SolidColorBrush x:Key="IconFill" Color="#231F20" />
<SolidColorBrush x:Key="StoplightRed" Color="#F06060" />
<SolidColorBrush x:Key="StoplightYellow" Color="#F0E160" />
<SolidColorBrush x:Key="StoplightGreen" Color="#70FA70" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#bed2fa" />
@@ -44,9 +41,6 @@
<SolidColorBrush x:Key="HyperlinkVisited" Color="Orchid" />
<SolidColorBrush x:Key="CancelRed" Color="#802727" />
<SolidColorBrush x:Key="IconFill" Color="#DCE0DF" />
<SolidColorBrush x:Key="StoplightRed" Color="#7d1f1f" />
<SolidColorBrush x:Key="StoplightYellow" Color="#7d7d1f" />
<SolidColorBrush x:Key="StoplightGreen" Color="#1f7d1f" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
@@ -11,7 +11,6 @@
<StreamGeometry x:Key="QueuedIcon">M31,0 H49 V100 H31 M58,0 H76 V100 H58 M85,0 H103 V100 H85 M8,85 V122 H129 V85 H117 V109 H20 V85 H8 M0,36 V66 L24,51 M114,36 V66 L138,51</StreamGeometry>
<StreamGeometry x:Key="QueueCompletedIcon">M0,0 H100 V100 H0 V0 M2,50 L36,82 L 93,27 L81,15 L36,59 L14,38</StreamGeometry>
<StreamGeometry x:Key="QueueErrorIcon">M0,0 H100 V100 H0 V0 M15,71 L29,85 L50,64 L71,85 L85,71 L64,50 L85,29 L71,15 L50,36 L29,15 L15,29 L36,50</StreamGeometry>
<StreamGeometry x:Key="BookErrorIcon">M32,0 a 32,32 0 0 1 0,64 a 32,32 0 0 1 0,-64 m 0,4 a 28,28 0 0 1 0,56 a 28,28 0 0 1 0,-56 m-21,24 h42 a 1,1 0 0 1 1,1 v6 a 1,1 0 0 1 -1,1 h-42 a 1,1 0 0 1 -1,-1 v-6 a 1,1 0 0 1 1,-1</StreamGeometry>
<RotateTransform x:Key="Rotate45Transform" Angle="45" />
@@ -23,40 +22,6 @@
M 78,6 L81.5,2.5 A 8,8 0 0 1 91.5,2 L98.5,9 A 8,8 0 0 1 97.5,19.5 L94,23
</StreamGeometry>
<StreamGeometry x:Key="CollapseIcon">
M0,2 A 2,2 0 0 1 2,0 H62 A2,2 0 0 1 64,2 V62 A 2,2 0 0 1 62,64 H 2 A 2,2 0 0 1 0,62 V2
M 2,2 H62 V62 H2 V2
M11,28 h42 a 1,1 0 0 1 1,1 v6 a 1,1 0 0 1 -1,1 h-42 a 1,1 0 0 1 -1,-1 v-6 a 1,1 0 0 1 1,-1
</StreamGeometry>
<StreamGeometry x:Key="VerticalBarIcon">M28,53 v-42 a 1,1 0 0 1 1,-1 h6 a 1,1 0 0 1 1,1 v42 a 1,1 0 0 1 -1,1 h-6 a 1,1 0 0 1 -1,-1</StreamGeometry>
<CombinedGeometry x:Key="ExpandIcon" Geometry1="{StaticResource CollapseIcon}" Geometry2="{StaticResource VerticalBarIcon}" />
<StreamGeometry x:Key="StoplightBodyIcon">
M0,12 A 12,12 0 0 1 12,0 H34 A 12,12 0 0 1 46,12 V88 A 12,12 0 0 1 34,100 H12 A 12,12 0 0 1 0,88 V12
M20,8 H26 A 12,12 0 0 1 26,32 H20 A 12,12 0 0 1 20,8
M20,38 H26 A 12,12 0 0 1 26,62 H20 A 12,12 0 0 1 20,38
M20,68 H26 A 12,12 0 0 1 26,92 H20 A 12,12 0 0 1 20,68
</StreamGeometry>
<StreamGeometry x:Key="PdfDownloadedIcon">
M4,38.5 H3 A 3,3 0 0 1 0,35.5 V21.4 A 3,3 0 0 1 3,18.4 H4 V2 A 2,2 0 0 1 6,0 H30.5 L41,12 V18.4 A 3,3 0 0 1 45,21.4 V35.5 A 3,3 0 0 1 42,38.5 H41 V48.5 A 2,2 0 0 1 39,50.5 H6 A 2,2 0 0 1 4,48.5
M6,38.5 H39 V48.5 H6 V38.5
M6,18.4 V2 H29 V12 A 1,1 0 0 0 30,13 H39 V18.4
M 4.3179,36 c 0,0 0.122,-14.969 0.122,-14.969 1.469,-0.194 2.939,-0.388 4.5,-0.362 1.561,0.026 3.214,0.27 4.357,0.944 1.143,0.674 1.775,1.776 2.015,2.959 0.24,1.184 0.087,2.449 -0.5,3.52 -0.587,1.071 -1.607,1.949 -2.816,2.352 -1.209,0.403 -2.607,0.332 -4.005,0.26 0,0 -0.031,5.265 -0.031,5.265 0,0 -3.673,0.122 -3.673,0.122 0,0 0.031,-0.092 0.031,-0.092
m 3.643,-12.428 c 0,0 0.031,4.286 0.031,4.286 0.735,0.051 1.47,0.102 2.107,-0.056 0.638,-0.158 1.178,-0.526 1.459,-1.122 0.281,-0.597 0.301,-1.423 0.01,-2.005 -0.291,-0.582 -0.893,-0.918 -1.546,-1.061 -0.653,-0.143 -1.357,-0.092 -1.709,-0.066 -0.352,0.026 -0.352,0.026 -0.352,0.026
m 9.428,12.428 c 2.265,0.245 4.531,0.49 6.674,0.066 2.143,-0.424 4.163,-1.515 5.285,-3.081 1.122,-1.566 1.347,-3.607 1.27,-5.306 -0.076,-1.699 -0.454,-3.056 -1.454,-4.219 -1,-1.163 -2.622,-2.133 -4.704,-2.505 -2.082,-0.373 -4.623,-0.148 -7.164,0.076 0,0 0.092,14.969 0.092,14.969
m 3.49,-12.398 c 0,0 0,9.673 0,9.673 0.888,0.02 1.776,0.041 2.653,-0.179 0.877,-0.219 1.745,-0.679 2.367,-1.541 0.622,-0.862 1,-2.127 0.98,-3.403 -0.02,-1.275 -0.439,-2.561 -1.193,-3.337 -0.755,-0.776 -1.847,-1.041 -2.704,-1.158 -0.857,-0.117 -1.48,-0.087 -2.102,-0.056
m 11.908,12.245 v-14.785 h8.969 v2.51 h-5.786 v3.612 h5.388 v2.51 h-5.449 v6.092
</StreamGeometry>
<StreamGeometry x:Key="PdfDownArrowIcon">
M29,44 V58.7498 H35.0491 A 1.5,1.5 0 0 1 36.1342,61.2861 L23.5607,73.8595 A 1.5,1.5 0 0 1 21.4393,73.8595 L8.8658,61.2861 A 1.5,1.5 0 0 1 9.9509,58.7498 H16 V44 A 1.5,1.5 0 0 1 17.5,42.5 H27.5 A 1.5,1.5 0 0 1 29,44
</StreamGeometry>
<CombinedGeometry x:Key="PdfNotDownloadedIcon" Geometry1="{StaticResource PdfDownloadedIcon}" Geometry2="{StaticResource PdfDownArrowIcon}" />
<StreamGeometry x:Key="ImportIcon">
M5.65,4.3 h-2.75 a2.9,2.25 0 0 0 -2.9,2.25 v7.2
a2.9,2.25 0 0 0 2.9,2.25 h10.2 a2.9,2.25 0 0 0 2.9,-2.25 v-7.2 a2.9,2.25 0 0 0 -2.9,-2.25
@@ -1,16 +1,10 @@
using ReactiveUI;
using Avalonia.Media;
using ReactiveUI;
namespace LibationAvalonia.ViewModels;
public class LiberateStatusButtonViewModel : ViewModelBase
{
public bool IsError { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
public bool IsButtonEnabled { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
public bool IsSeries { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
public bool Expanded { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
public bool RedVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = true;
public bool YellowVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
public bool GreenVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
public bool PdfDownloadedVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
public bool PdfNotDownloadedVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
public IImage? ButtonImage { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
}
@@ -1,6 +1,6 @@
using LibationFileManager;
using Avalonia.Styling;
using LibationFileManager;
using LibationUiBase;
using System;
using System.IO;
namespace LibationAvalonia.ViewModels;
@@ -23,19 +23,6 @@ partial class MainVM
PictureStorage.SetDefaultImage(PictureSize.Native, ms3.ToArray());
BaseUtil.SetLoadImageDelegate(AvaloniaUtils.TryLoadImageOrDefault);
BaseUtil.SetLoadResourceImageDelegate(LoadResourceImage);
}
private static Avalonia.Media.Imaging.Bitmap? LoadResourceImage(string resourceName)
{
try
{
using var stream = App.OpenAsset(resourceName);
return new Avalonia.Media.Imaging.Bitmap(stream);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Failed to load resource image: {ResourceName}", resourceName);
return null;
}
BaseUtil.SetIsDarkModeDelegate(() => App.Current.ActualThemeVariant == ThemeVariant.Dark);
}
}
@@ -9,22 +9,6 @@
x:Class="LibationAvalonia.Views.LiberateStatusButton">
<UserControl.Styles>
<Style Selector="Path">
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
<Setter Property="Stretch" Value="Uniform" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Rectangle">
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="18" />
</Style>
<Style Selector="Grid > Path">
<Setter Property="Margin" Value="4,0,0,0" />
<Setter Property="Width" Value="28.8" />
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SystemChromeDisabledLowColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemChromeDisabledLowColor}" />
@@ -39,41 +23,11 @@
IsEnabled="{Binding IsButtonEnabled}" Click="Button_Click" >
<Grid RowDefinitions="*,8*,*">
<Viewbox
<Image
Grid.Row="1"
Stretch="Uniform"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Panel>
<Panel IsVisible="{Binding !IsError}">
<Panel IsVisible="{Binding IsSeries}">
<Path IsVisible="{Binding Expanded}" Data="{StaticResource CollapseIcon}" />
<Path IsVisible="{Binding !Expanded}" Data="{StaticResource ExpandIcon}" />
</Panel>
<Grid
IsVisible="{Binding !IsSeries}"
HorizontalAlignment="Center"
ColumnDefinitions="Auto,Auto">
<Canvas Width="29.44" Height="64">
<Rectangle Canvas.Left="5" Canvas.Top="5" IsVisible="{Binding RedVisible}" Fill="{DynamicResource StoplightRed}" />
<Rectangle Canvas.Left="5" Canvas.Top="23" IsVisible="{Binding YellowVisible}" Fill="{DynamicResource StoplightYellow}" />
<Rectangle Canvas.Left="5" Canvas.Top="42" IsVisible="{Binding GreenVisible}" Fill="{DynamicResource StoplightGreen}" />
<Path Height="64" Stretch="Uniform" Data="{StaticResource StoplightBodyIcon}"/>
</Canvas>
<Path Grid.Column="1" IsVisible="{Binding PdfDownloadedVisible}" Data="{StaticResource PdfDownloadedIcon}"/>
<Path Grid.Column="1" IsVisible="{Binding PdfNotDownloadedVisible}" Data="{StaticResource PdfNotDownloadedIcon}"/>
</Grid>
</Panel>
<Path
Stretch="None" Width="64"
IsVisible="{Binding IsError}"
Fill="{DynamicResource CancelRed}"
Data="{StaticResource BookErrorIcon}" />
</Panel>
</Viewbox>
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding ButtonImage}" />
</Grid>
</Button>
</UserControl>
@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase.GridView;
@@ -21,17 +22,15 @@ public partial class LiberateStatusButton : UserControl
public static readonly StyledProperty<bool> IsUnavailableProperty =
AvaloniaProperty.Register<LiberateStatusButton, bool>(nameof(IsUnavailable));
public static readonly StyledProperty<bool> ExpandedProperty =
AvaloniaProperty.Register<LiberateStatusButton, bool>(nameof(Expanded));
public static readonly StyledProperty<bool> IsSeriesProperty =
AvaloniaProperty.Register<LiberateStatusButton, bool>(nameof(IsSeries));
public static readonly StyledProperty<IImage?> ButtonImageProperty =
AvaloniaProperty.Register<LiberateStatusButton, IImage?>(nameof(ButtonImage));
public LiberatedStatus BookStatus { get => GetValue(BookStatusProperty); set => SetValue(BookStatusProperty, value); }
public LiberatedStatus? PdfStatus { get => GetValue(PdfStatusProperty); set => SetValue(PdfStatusProperty, value); }
public bool IsUnavailable { get => GetValue(IsUnavailableProperty); set => SetValue(IsUnavailableProperty, value); }
public bool Expanded { get => GetValue(ExpandedProperty); set => SetValue(ExpandedProperty, value); }
public bool IsSeries { get => GetValue(IsSeriesProperty); set => SetValue(IsSeriesProperty, value); }
/// <summary>The shared rendering of this entry's status, from <see cref="EntryStatus.ButtonImage"/>.</summary>
public IImage? ButtonImage { get => GetValue(ButtonImageProperty); set => SetValue(ButtonImageProperty, value); }
private readonly LiberateStatusButtonViewModel viewModel = new();
@@ -40,14 +39,10 @@ public partial class LiberateStatusButton : UserControl
InitializeComponent();
button.DataContext = viewModel;
if (Design.IsDesignMode)
{
BookStatus = LiberatedStatus.PartialDownload;
PdfStatus = null;
IsSeries = true;
}
DataContextChanged += LiberateStatusButton_DataContextChanged;
//The icon is rendered for a specific theme, so it has to be re-rendered when the theme changes.
ActualThemeVariantChanged += (_, _) => (DataContext as GridEntry)?.Liberate?.Invalidate(nameof(EntryStatus.ButtonImage));
}
private void LiberateStatusButton_DataContextChanged(object? sender, EventArgs e)
@@ -55,35 +50,17 @@ public partial class LiberateStatusButton : UserControl
//Force book status recheck when an entry is scrolled into view.
//This will force a recheck for a partially downloaded file.
var status = DataContext as LibraryBookEntry;
status?.Liberate?.Invalidate(nameof(status.Liberate.BookStatus));
status?.Liberate?.Invalidate(nameof(status.Liberate.BookStatus), nameof(status.Liberate.ButtonImage));
}
private void Button_Click(object sender, RoutedEventArgs e) => Click?.Invoke(this, EventArgs.Empty);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == BookStatusProperty)
{
viewModel.IsError = BookStatus is LiberatedStatus.Error;
viewModel.RedVisible = BookStatus is LiberatedStatus.NotLiberated;
viewModel.YellowVisible = BookStatus is LiberatedStatus.PartialDownload;
viewModel.GreenVisible = BookStatus is LiberatedStatus.Liberated;
}
else if (change.Property == PdfStatusProperty)
{
viewModel.PdfDownloadedVisible = PdfStatus is LiberatedStatus.Liberated;
viewModel.PdfNotDownloadedVisible = PdfStatus is LiberatedStatus.NotLiberated;
}
else if (change.Property == IsSeriesProperty)
{
viewModel.IsSeries = IsSeries;
}
else if (change.Property == ExpandedProperty)
{
viewModel.Expanded = Expanded;
}
if (change.Property == ButtonImageProperty)
viewModel.ButtonImage = ButtonImage;
viewModel.IsButtonEnabled = !viewModel.IsError && (!IsUnavailable || (BookStatus is LiberatedStatus.Liberated && PdfStatus is null or LiberatedStatus.Liberated));
viewModel.IsButtonEnabled = BookStatus is not LiberatedStatus.Error && (!IsUnavailable || (BookStatus is LiberatedStatus.Liberated && PdfStatus is null or LiberatedStatus.Liberated));
base.OnPropertyChanged(change);
}
@@ -110,8 +110,7 @@
BookStatus="{Binding Liberate.BookStatus}"
PdfStatus="{Binding Liberate.PdfStatus}"
IsUnavailable="{Binding Liberate.IsUnavailable}"
IsSeries="{Binding Liberate.IsSeries}"
Expanded="{Binding Liberate.Expanded}"
ButtonImage="{Binding Liberate.ButtonImage}"
Click="LiberateButton_Click" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>