Add support for custom themes in chardonnay

This commit is contained in:
Michael Bucari-Tovo 2025-03-11 17:31:15 -06:00
parent a37eb383cd
commit b34970bd47
26 changed files with 719 additions and 280 deletions

View File

@ -20,6 +20,13 @@
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}"> <ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" /> <Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
</ControlTheme> </ControlTheme>
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
<Setter Property="Padding" Value="6,0,0,0" />
</ControlTheme>
<x:Double x:Key="DataGridSortIconMinWidth">0</x:Double>
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
<ResourceDictionary.ThemeDictionaries> <ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light"> <ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" /> <SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
@ -28,17 +35,11 @@
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="Khaki" /> <SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="Khaki" />
<SolidColorBrush x:Key="HyperlinkNew" Color="Blue" /> <SolidColorBrush x:Key="HyperlinkNew" Color="Blue" />
<SolidColorBrush x:Key="HyperlinkVisited" Color="Purple" /> <SolidColorBrush x:Key="HyperlinkVisited" Color="Purple" />
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="White" />
<SolidColorBrush x:Key="SystemOpaqueBase" Color="White" />
<SolidColorBrush x:Key="CancelRed" Color="FireBrick" /> <SolidColorBrush x:Key="CancelRed" Color="FireBrick" />
<SolidColorBrush x:Key="IconFill" Color="#231F20" /> <SolidColorBrush x:Key="IconFill" Color="#231F20" />
<SolidColorBrush x:Key="StoplightRed" Color="#F06060" /> <SolidColorBrush x:Key="StoplightRed" Color="#F06060" />
<SolidColorBrush x:Key="StoplightYellow" Color="#F0E160" /> <SolidColorBrush x:Key="StoplightYellow" Color="#F0E160" />
<SolidColorBrush x:Key="StoplightGreen" Color="#70FA70" /> <SolidColorBrush x:Key="StoplightGreen" Color="#70FA70" />
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
</ResourceDictionary> </ResourceDictionary>
<ResourceDictionary x:Key="Dark"> <ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#bed2fa" /> <SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#bed2fa" />
@ -47,29 +48,26 @@
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="#4e4b15" /> <SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="#4e4b15" />
<SolidColorBrush x:Key="HyperlinkNew" Color="CornflowerBlue" /> <SolidColorBrush x:Key="HyperlinkNew" Color="CornflowerBlue" />
<SolidColorBrush x:Key="HyperlinkVisited" Color="Orchid" /> <SolidColorBrush x:Key="HyperlinkVisited" Color="Orchid" />
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="Black" />
<SolidColorBrush x:Key="SystemOpaqueBase" Color="Black" />
<SolidColorBrush x:Key="CancelRed" Color="#802727" /> <SolidColorBrush x:Key="CancelRed" Color="#802727" />
<SolidColorBrush x:Key="IconFill" Color="#DCE0DF" /> <SolidColorBrush x:Key="IconFill" Color="#DCE0DF" />
<SolidColorBrush x:Key="StoplightRed" Color="#7d1f1f" /> <SolidColorBrush x:Key="StoplightRed" Color="#7d1f1f" />
<SolidColorBrush x:Key="StoplightYellow" Color="#7d7d1f" /> <SolidColorBrush x:Key="StoplightYellow" Color="#7d7d1f" />
<SolidColorBrush x:Key="StoplightGreen" Color="#1f7d1f" /> <SolidColorBrush x:Key="StoplightGreen" Color="#1f7d1f" />
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
</ResourceDictionary> </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries> </ResourceDictionary.ThemeDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
<Application.Styles> <Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/> <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
<StyleInclude Source="/Assets/LibationVectorIcons.xaml"/> <StyleInclude Source="/Assets/LibationVectorIcons.xaml"/>
<StyleInclude Source="/Assets/DataGridColumnHeader.xaml"/> <FluentTheme>
<FluentTheme.Palettes>
<ColorPaletteResources x:Key="Light" />
<ColorPaletteResources x:Key="Dark" />
</FluentTheme.Palettes>
</FluentTheme>
<Style Selector="TextBox[IsReadOnly=true]"> <Style Selector="TextBox[IsReadOnly=true]">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" /> <Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
@ -95,7 +93,7 @@
<Setter Property="SystemDecorations" Value="Full"/> <Setter Property="SystemDecorations" Value="Full"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<ContentPresenter Background="{DynamicResource SystemControlBackgroundAltHighBrush}" Content="{TemplateBinding Content}" /> <ContentPresenter Background="{DynamicResource SystemRegionColor}" Content="{TemplateBinding Content}" />
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>
@ -106,7 +104,7 @@
<Setter Property="SystemDecorations" Value="BorderOnly"/> <Setter Property="SystemDecorations" Value="BorderOnly"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Name="DialogWindowFormBorder" BorderBrush="{DynamicResource SystemBaseMediumLowColor}" Background="{DynamicResource SystemControlBackgroundAltHighBrush}"> <Border Name="DialogWindowFormBorder" BorderBrush="{DynamicResource SystemBaseMediumLowColor}" Background="{DynamicResource SystemRegionColor}">
<Grid RowDefinitions="30,*"> <Grid RowDefinitions="30,*">
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}"> <Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
<Border.Styles> <Border.Styles>

View File

@ -2,7 +2,6 @@
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using LibationAvalonia.Dialogs; using LibationAvalonia.Dialogs;
@ -13,19 +12,22 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Threading; using Avalonia.Threading;
using Dinah.Core;
using LibationAvalonia.Themes;
using Avalonia.Data.Core.Plugins;
using System.Linq;
#nullable enable
namespace LibationAvalonia namespace LibationAvalonia
{ {
public class App : Application public class App : Application
{ {
public static MainWindow MainWindow { get; private set; } public static Task<List<DataLayer.LibraryBook>>? LibraryTask { get; set; }
public static IBrush ProcessQueueBookFailedBrush { get; private set; } public static ChardonnayTheme? DefaultThemeColors { get; private set; }
public static IBrush ProcessQueueBookCompletedBrush { get; private set; } public static MainWindow? MainWindow { get; private set; }
public static IBrush ProcessQueueBookCancelledBrush { get; private set; } public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/");
public static IBrush ProcessQueueBookDefaultBrush { get; private set; } public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet.");
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
public static Stream OpenAsset(string assetRelativePath) public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath)); => AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
@ -34,12 +36,16 @@ namespace LibationAvalonia
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
public static Task<List<DataLayer.LibraryBook>> LibraryTask;
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
DefaultThemeColors = ChardonnayTheme.GetLiveTheme();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
var config = Configuration.Instance; var config = Configuration.Instance;
if (!config.LibationSettingsAreValid) if (!config.LibationSettingsAreValid)
@ -69,11 +75,23 @@ namespace LibationAvalonia
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
private void DisableAvaloniaDataAnnotationValidation()
private async void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{ {
var setupDialog = sender as SetupDialog; // Get an array of plugins to remove
var desktop = ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
private async void Setup_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
if (sender is not SetupDialog setupDialog || ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return;
try try
{ {
@ -87,7 +105,7 @@ namespace LibationAvalonia
if (setupDialog.Config.LibationSettingsAreValid) if (setupDialog.Config.LibationSettingsAreValid)
{ {
string theme = setupDialog.SelectedTheme.Content as string; string? theme = setupDialog.SelectedTheme.Content as string;
setupDialog.Config.SetString(theme, nameof(ThemeVariant)); setupDialog.Config.SetString(theme, nameof(ThemeVariant));
@ -143,7 +161,7 @@ namespace LibationAvalonia
desktop.MainWindow = libationFilesDialog; desktop.MainWindow = libationFilesDialog;
libationFilesDialog.Show(); libationFilesDialog.Show();
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e) void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e)
{ {
libationFilesDialog.Closing -= WindowClosing; libationFilesDialog.Closing -= WindowClosing;
e.Cancel = true; e.Cancel = true;
@ -201,16 +219,9 @@ namespace LibationAvalonia
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop) private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
{ {
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) switch Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
{ OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
nameof(ThemeVariant.Light) => ThemeVariant.Light,
// "System"
_ => ThemeVariant.Default
};
//Reload colors for current theme
LoadStyles();
var mainWindow = new MainWindow(); var mainWindow = new MainWindow();
desktop.MainWindow = MainWindow = mainWindow; desktop.MainWindow = MainWindow = mainWindow;
mainWindow.Loaded += MainWindow_Loaded; mainWindow.Loaded += MainWindow_Loaded;
@ -218,19 +229,23 @@ namespace LibationAvalonia
mainWindow.Show(); mainWindow.Show();
} }
private static async void MainWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e) [PropertyChangeFilter(nameof(ThemeVariant))]
private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
=> OpenAndApplyTheme(e.NewValue as string);
private static void OpenAndApplyTheme(string? themeVariant)
{ {
var library = await LibraryTask; using var themePersister = ChardonnayThemePersister.Create();
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library)); themePersister?.Target.ApplyTheme(themeVariant);
} }
private static void LoadStyles() private static async void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush)); if (LibraryTask is not null && MainWindow is not null)
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCompletedBrush)); {
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCancelledBrush)); var library = await LibraryTask;
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources(nameof(SeriesEntryGridBackgroundBrush)); await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookDefaultBrush)); }
} }
} }
} }

View File

@ -1,104 +0,0 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:collections="using:Avalonia.Collections">
<Styles.Resources>
<!--
Based on Fluent template from v11.0.0-preview8
Modified sort arrow positioning to make more room for header text
-->
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader">
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="FontSize" Value="12" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="HeaderBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
<Grid Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="16" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon"
IsVisible="False"
Grid.Column="1"
Height="12"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Fill="{TemplateBinding Foreground}"
Stretch="Uniform" />
</Grid>
<Rectangle Name="VerticalSeparator"
Grid.Column="1"
Width="1"
VerticalAlignment="Stretch"
Fill="{TemplateBinding SeparatorBrush}"
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
<Grid x:Name="FocusVisual" IsHitTestVisible="False"
IsVisible="False">
<Rectangle x:Name="FocusVisualPrimary"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle x:Name="FocusVisualSecondary"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:focus-visible /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:pointerover /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
</Style>
<Style Selector="^:pressed /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
</Style>
<Style Selector="^:dragIndicator">
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style Selector="^:sortascending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{StaticResource DataGridSortIconAscendingPath}" />
</Style>
<Style Selector="^:sortdescending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{StaticResource DataGridSortIconDescendingPath}" />
</Style>
</ControlTheme>
</Styles.Resources>
</Styles>

View File

@ -1,8 +1,8 @@
using Avalonia.Controls; using Avalonia;
using Avalonia.Media; using Avalonia.Controls;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using LibationAvalonia.Dialogs;
using LibationFileManager; using LibationFileManager;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -11,17 +11,21 @@ namespace LibationAvalonia
{ {
internal static class AvaloniaUtils internal static class AvaloniaUtils
{ {
public static IBrush GetBrushFromResources(string name) public static T DynamicResource<T>(this T control, AvaloniaProperty prop, object resourceKey) where T : Control
=> GetBrushFromResources(name, Brushes.Transparent);
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
{ {
if ((App.Current?.TryGetResource(name, App.Current.ActualThemeVariant, out var value) ?? false) && value is IBrush brush) control[!prop] = new DynamicResourceExtension(resourceKey);
return brush; return control;
return defaultBrush;
} }
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window? owner = null) public static Task<DialogResult> ShowDialogAsync(this Dialogs.DialogWindow dialogWindow, Window? owner = null)
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow); => ((owner ?? App.MainWindow) is Window window)
? dialogWindow.ShowDialog<DialogResult>(window)
: Task.FromResult(DialogResult.None);
public static Task<DialogResult> ShowDialogAsync(this Dialogs.Login.WebLoginDialog dialogWindow, Window? owner = null)
=> ((owner ?? App.MainWindow) is Window window)
? dialogWindow.ShowDialog<DialogResult>(window)
: Task.FromResult(DialogResult.None);
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window; public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;

View File

@ -28,7 +28,7 @@
<TextBlock <TextBlock
Name="PART_Label" Name="PART_Label"
Padding="4,0" Padding="4,0"
Background="{DynamicResource SystemAltHighColor}" Background="{DynamicResource SystemRegionColor}"
Text="{TemplateBinding Label}" Text="{TemplateBinding Label}"
/> />
</Grid> </Grid>

View File

@ -158,15 +158,24 @@
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
FontSize="16" FontSize="16"
Margin="0,0,15,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Theme: "/> Text="Theme:"/>
<controls:WheelComboBox <controls:WheelComboBox
Name="ThemeComboBox"
Grid.Column="1" Grid.Column="1"
MinWidth="80" MinWidth="80"
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}" SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
ItemsSource="{CompiledBinding Themes}"/> ItemsSource="{CompiledBinding Themes}"/>
<Button
Grid.Column="2"
HorizontalAlignment="Right"
Padding="20,0"
VerticalAlignment="Stretch"
Content="Edit Theme Colors"
Click="EditThemeColors_Click"/>
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -1,13 +1,18 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Dinah.Core; using Dinah.Core;
using FileManager; using FileManager;
using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Settings; using LibationAvalonia.ViewModels.Settings;
using LibationFileManager; using LibationFileManager;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Controls.Settings namespace LibationAvalonia.Controls.Settings
{ {
public partial class Important : UserControl public partial class Important : UserControl
{ {
private ImportantSettingsVM? ViewModel => DataContext as ImportantSettingsVM;
public Important() public Important()
{ {
InitializeComponent(); InitializeComponent();
@ -16,6 +21,42 @@ namespace LibationAvalonia.Controls.Settings
_ = Configuration.Instance.LibationFiles; _ = Configuration.Instance.LibationFiles;
DataContext = new ImportantSettingsVM(Configuration.Instance); DataContext = new ImportantSettingsVM(Configuration.Instance);
} }
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
}
private void EditThemeColors_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (App.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
//Only allow a single instance of the theme picker
//Show it as a window, not a dialog, so users can preview
//their changes throughout the entire app.
if (lifetime.Windows.OfType<ThemePickerDialog>().FirstOrDefault() is ThemePickerDialog dialog)
{
dialog.BringIntoView();
}
else
{
var themePicker = new ThemePickerDialog();
themePicker.Show();
}
}
}
private void ThemeComboBox_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
//Remove the combo box before changing the theme, then re-add it.
//This is a workaround to a crash that will happen if the theme
//is changed while the combo box is open
ThemeComboBox.SelectionChanged -= ThemeComboBox_SelectionChanged;
var parent = ThemeComboBox.Parent as Panel;
if (parent?.Children.Remove(ThemeComboBox) ?? false)
{
Configuration.Instance.SetString(ViewModel?.ThemeVariant, nameof(ViewModel.ThemeVariant));
parent.Children.Add(ThemeComboBox);
}
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
} }
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)

View File

@ -10,6 +10,8 @@ namespace LibationAvalonia.Dialogs
{ {
public abstract class DialogWindow : Window public abstract class DialogWindow : Window
{ {
protected bool CancelOnEscape { get; set; } = true;
protected bool SaveOnEnter { get; set; } = true;
public bool SaveAndRestorePosition { get; set; } = true; public bool SaveAndRestorePosition { get; set; } = true;
public Control ControlToFocusOnShow { get; set; } public Control ControlToFocusOnShow { get; set; }
protected override Type StyleKeyOverride => typeof(DialogWindow); protected override Type StyleKeyOverride => typeof(DialogWindow);
@ -132,9 +134,9 @@ namespace LibationAvalonia.Dialogs
private async void DialogWindow_KeyDown(object sender, Avalonia.Input.KeyEventArgs e) private async void DialogWindow_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
{ {
if (e.Key == Avalonia.Input.Key.Escape) if (CancelOnEscape && e.Key == Avalonia.Input.Key.Escape)
await CancelAndCloseAsync(); await CancelAndCloseAsync();
else if (e.Key == Avalonia.Input.Key.Return) else if (SaveOnEnter && e.Key == Avalonia.Input.Key.Return)
await SaveAndCloseAsync(); await SaveAndCloseAsync();
} }
} }

View File

@ -1,10 +1,10 @@
using AudibleApi; using AudibleApi;
using AudibleUtilities; using AudibleUtilities;
using Avalonia.Threading;
using LibationFileManager; using LibationFileManager;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Dialogs.Login namespace LibationAvalonia.Dialogs.Login
{ {
public class AvaloniaLoginChoiceEager : ILoginChoiceEager public class AvaloniaLoginChoiceEager : ILoginChoiceEager
@ -23,14 +23,14 @@ namespace LibationAvalonia.Dialogs.Login
LoginCallback = new AvaloniaLoginCallback(_account); LoginCallback = new AvaloniaLoginCallback(_account);
} }
public async Task<ChoiceOut> StartAsync(ChoiceIn choiceIn) public async Task<ChoiceOut?> StartAsync(ChoiceIn choiceIn)
{ {
if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10) if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
{ {
try try
{ {
var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl); var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
if (await weblogin.ShowDialog<DialogResult>(App.MainWindow) is DialogResult.OK) if (await weblogin.ShowDialogAsync(App.MainWindow) is DialogResult.OK)
return ChoiceOut.External(weblogin.ResponseUrl); return ChoiceOut.External(weblogin.ResponseUrl);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -0,0 +1,70 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="450"
Width="450" Height="450"
x:Class="LibationAvalonia.Dialogs.ThemePickerDialog"
Title="Theme Editor">
<Grid
RowDefinitions="*,Auto">
<DataGrid
GridLinesVisibility="All"
Margin="5"
IsReadOnly="False"
ItemsSource="{Binding ThemeColors}">
<DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Color">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ColorPicker
IsHexInputVisible="True"
Color="{Binding ThemeColor, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
Binding="{Binding ThemeItemName, Mode=TwoWay}"
Header="Theme Item"/>
</DataGrid.Columns>
</DataGrid>
<Grid
Grid.Row="1"
ColumnDefinitions="Auto,Auto,Auto,*,Auto">
<Grid.Styles>
<Style Selector="Button">
<Setter Property="Height" Value="30" />
<Setter Property="Padding" Value="20,0" />
<Setter Property="Margin" Value="5" />
</Style>
</Grid.Styles>
<Button
Grid.Column="0"
Content="Cancel"
Command="{Binding CancelAndClose}" />
<Button
Grid.Column="1"
Content="Reset"
Command="{Binding ResetColors}" />
<Button
Grid.Column="2"
Content="Defaults"
Command="{Binding LoadDefaultColors}" />
<Button
Grid.Column="4"
Content="Save"
Command="{Binding SaveAndCloseAsync}" />
</Grid>
</Grid>
</Window>

View File

@ -0,0 +1,109 @@
using Avalonia.Collections;
using Avalonia.Media;
using ReactiveUI;
using Avalonia.Styling;
using System;
using LibationAvalonia.Themes;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Dialogs;
public partial class ThemePickerDialog : DialogWindow
{
protected DataGridCollectionView ThemeColors { get; }
private ChardonnayTheme ExistingTheme { get; } = ChardonnayTheme.GetLiveTheme();
public ThemePickerDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
CancelOnEscape = false;
var workingTheme = (ChardonnayTheme)ExistingTheme.Clone();
ThemeColors = new(EnumerateThemeItemColors(workingTheme, ActualThemeVariant));
DataContext = this;
}
protected override void CancelAndClose()
{
ExistingTheme.ApplyTheme(ActualThemeVariant);
base.CancelAndClose();
}
protected void ResetColors()
=> ResetTheme(ExistingTheme);
protected void LoadDefaultColors()
{
if (App.DefaultThemeColors is ChardonnayTheme defaults)
ResetTheme(defaults);
}
protected override async Task SaveAndCloseAsync()
{
using (var themePersister = ChardonnayThemePersister.Create())
{
if (themePersister is null)
{
await MessageBox.Show(this, "Failed to save the theme.", "Error saving theme", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
foreach (var i in ThemeColors.OfType<ThemeItemColor>())
{
themePersister.Target.SetColor(ActualThemeVariant, i.ThemeItemName, i.ThemeColor);
}
themePersister.Target.Save();
}
}
await base.SaveAndCloseAsync();
}
private void ResetTheme(ChardonnayTheme theme)
{
theme.ApplyTheme(ActualThemeVariant);
foreach (var i in ThemeColors.OfType<ThemeItemColor>())
{
i.SuppressSet = true;
i.ThemeColor = theme.GetColor(ActualThemeVariant, i.ThemeItemName);
i.SuppressSet = false;
}
}
private static IEnumerable<ThemeItemColor> EnumerateThemeItemColors(ChardonnayTheme workingTheme, ThemeVariant themeVariant)
=> workingTheme
.GetThemeColors(themeVariant)
.Select(kvp => new ThemeItemColor
{
ThemeItemName = kvp.Key,
ThemeColor = kvp.Value,
ColorSetter = c =>
{
workingTheme.SetColor(themeVariant, kvp.Key, c);
workingTheme.ApplyTheme(themeVariant);
}
});
private class ThemeItemColor : ViewModels.ViewModelBase
{
public bool SuppressSet { get; set; }
public required string ThemeItemName { get; init; }
public required Action<Color> ColorSetter { get; init; }
private Color _themeColor;
public Color ThemeColor
{
get => _themeColor;
set
{
var setColors = !SuppressSet && !_themeColor.Equals(value);
this.RaiseAndSetIfChanged(ref _themeColor, value);
if (setColors)
ColorSetter?.Invoke(_themeColor);
}
}
}
}

View File

@ -37,7 +37,6 @@
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="Assets\**" /> <AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" /> <None Remove=".gitignore" />
<None Remove="Assets\DataGridColumnHeader.xaml" />
<None Remove="Assets\img-coverart-prod-unavailable_300x300.jpg" /> <None Remove="Assets\img-coverart-prod-unavailable_300x300.jpg" />
<None Remove="Assets\img-coverart-prod-unavailable_500x500.jpg" /> <None Remove="Assets\img-coverart-prod-unavailable_500x500.jpg" />
<None Remove="Assets\img-coverart-prod-unavailable_80x80.jpg" /> <None Remove="Assets\img-coverart-prod-unavailable_80x80.jpg" />
@ -74,6 +73,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.2.5" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.5" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" /> <PackageReference Include="Avalonia.Diagnostics" Version="11.2.5" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.2.5" /> <PackageReference Include="Avalonia" Version="11.2.5" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.5" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.5" />

View File

@ -0,0 +1,207 @@
using Avalonia.Media;
using System.Collections.Generic;
using Avalonia.Styling;
using System;
using Dinah.Core;
using Newtonsoft.Json;
using Avalonia.Controls;
using Avalonia.Themes.Fluent;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.Frozen;
#nullable enable
namespace LibationAvalonia;
public class ChardonnayTheme : IUpdatable, ICloneable
{
public event EventHandler? Updated;
/// <summary>Theme color overrides</summary>
[JsonProperty]
private readonly Dictionary<ThemeVariant, Dictionary<string, Color>> ThemeColors;
/// <summary>The two theme variants supported by Fluent themes</summary>
private static readonly FrozenSet<ThemeVariant> FluentVariants = [ThemeVariant.Light, ThemeVariant.Dark];
/// <summary>Reusable color pallets for the two theme variants</summary>
private static readonly FrozenDictionary<ThemeVariant, ColorPaletteResources> ColorPalettes
= FluentVariants.ToFrozenDictionary(t => t, _ => new ColorPaletteResources());
private ChardonnayTheme()
{
ThemeColors = FluentVariants.ToDictionary(t => t, _ => new Dictionary<string, Color>());
}
/// <summary> Invoke <see cref="IUpdatable.Updated"/> </summary>
public void Save() => Updated?.Invoke(this, EventArgs.Empty);
public Color GetColor(string? themeVariant, string itemName)
=> GetColor(FromVariantName(themeVariant), itemName);
public Color GetColor(ThemeVariant themeVariant, string itemName)
{
ValidateThemeVariant(themeVariant);
return ThemeColors[themeVariant].TryGetValue(itemName, out var color) ? color : default;
}
public ChardonnayTheme SetColor(string? themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
=> SetColor(FromVariantName(themeVariant), colorSelector, color);
public ChardonnayTheme SetColor(ThemeVariant themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
{
if (colorSelector.Body.NodeType is ExpressionType.MemberAccess &&
colorSelector.Body is MemberExpression memberExpression &&
memberExpression.Member is PropertyInfo colorProperty &&
colorProperty.DeclaringType == typeof(ColorPaletteResources))
return SetColor(themeVariant, colorProperty.Name, color);
return this;
}
public ChardonnayTheme SetColor(string? themeVariant, string itemName, Color itemColor)
=> SetColor(FromVariantName(themeVariant), itemName, itemColor);
public ChardonnayTheme SetColor(ThemeVariant themeVariant, string itemName, Color itemColor)
{
ValidateThemeVariant(themeVariant);
ThemeColors[themeVariant][itemName] = itemColor;
return this;
}
public FrozenDictionary<string, Color> GetThemeColors(string? themeVariant)
=> GetThemeColors(FromVariantName(themeVariant));
public FrozenDictionary<string, Color> GetThemeColors(ThemeVariant themeVariant)
{
ValidateThemeVariant(themeVariant);
return ThemeColors[themeVariant].ToFrozenDictionary();
}
public void ApplyTheme(string? themeVariant)
=> ApplyTheme(FromVariantName(themeVariant));
public void ApplyTheme(ThemeVariant themeVariant)
{
App.Current.RequestedThemeVariant = themeVariant;
themeVariant = App.Current.ActualThemeVariant;
ValidateThemeVariant(themeVariant);
bool fluentColorChanged = false;
//Set the Libation-specific brushes
var themeBrushes = (ResourceDictionary)App.Current.Resources.ThemeDictionaries[themeVariant];
foreach (var colorName in themeBrushes.Keys.OfType<string>())
{
if (ThemeColors[themeVariant].TryGetValue(colorName, out var color) && color != default)
{
if (themeBrushes[colorName] is ISolidColorBrush brush && brush.Color != color)
themeBrushes[colorName] = new SolidColorBrush(color);
}
}
//Set the fluent theme colors
foreach (var p in GetColorResourceProperties())
{
if (ThemeColors[themeVariant].TryGetValue(p.Name, out var color) && color != default)
{
if (p.GetValue(ColorPalettes[themeVariant]) is not Color c || c != color)
{
p.SetValue(ColorPalettes[themeVariant], color);
fluentColorChanged = true;
}
}
}
if (fluentColorChanged)
{
var oldFluent = App.Current.Styles.OfType<FluentTheme>().Single();
App.Current.Styles.Remove(oldFluent);
//We must make a new fluent theme and add it to the app for
//the changes to the ColorPaletteResources to take effect.
//Changes to the Libation-specific resources are instant.
var newFluent = new FluentTheme();
foreach (var kvp in ColorPalettes)
newFluent.Palettes[kvp.Key] = kvp.Value;
App.Current.Styles.Add(newFluent);
}
}
/// <summary> Get the currently-active theme colors. </summary>
public static ChardonnayTheme GetLiveTheme()
{
var theme = new ChardonnayTheme();
foreach (var themeVariant in FluentVariants)
{
//Get the Libation-specific brushes
var themeBrushes = (ResourceDictionary)App.Current.Resources.ThemeDictionaries[themeVariant];
foreach (var colorName in themeBrushes.Keys.OfType<string>())
{
if (themeBrushes[colorName] is ISolidColorBrush brush)
{
//We're only working with colors, so convert the Brush's opacity to an alpha value
var color = Color.FromArgb
(
(byte)Math.Round(brush.Color.A * brush.Opacity, 0),
brush.Color.R,
brush.Color.G,
brush.Color.B
);
theme.ThemeColors[themeVariant][colorName] = color;
}
}
//Get the fluent theme colors
foreach (var p in GetColorResourceProperties())
{
var color = (Color)p.GetValue(ColorPalettes[themeVariant])!;
//The color isn't being overridden, so get the static resource value.
if (color == default)
{
var staticResourceName = p.Name == nameof(ColorPaletteResources.RegionColor) ? "SystemRegionColor" : $"System{p.Name}Color";
if (App.Current.TryGetResource(staticResourceName, themeVariant, out var colorObj) && colorObj is Color c)
color = c;
}
theme.ThemeColors[themeVariant][p.Name] = color;
}
}
return theme;
}
public object Clone()
{
var clone = new ChardonnayTheme();
foreach (var t in ThemeColors)
{
clone.ThemeColors[t.Key] = t.Value.ToDictionary();
}
return clone;
}
private static IEnumerable<PropertyInfo> GetColorResourceProperties()
=> typeof(ColorPaletteResources).GetProperties().Where(p => p.PropertyType == typeof(Color));
[System.Diagnostics.StackTraceHidden]
private static void ValidateThemeVariant(ThemeVariant themeVariant)
{
if (!FluentVariants.Contains(themeVariant))
throw new InvalidOperationException("FluentTheme.Palettes only supports Light and Dark variants.");
}
private static ThemeVariant FromVariantName(string? variantName)
=> variantName switch
{
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
nameof(ThemeVariant.Light) => ThemeVariant.Light,
// "System"
_ => ThemeVariant.Default
};
}

View File

@ -0,0 +1,61 @@
using Avalonia.Media;
using Dinah.Core.IO;
using FileManager;
using LibationFileManager;
using Newtonsoft.Json;
using System;
#nullable enable
namespace LibationAvalonia.Themes;
public class ChardonnayThemePersister : JsonFilePersister<ChardonnayTheme>
{
public static string jsonPath = System.IO.Path.Combine(Configuration.Instance.LibationFiles, "ChardonnayTheme.json");
private ChardonnayThemePersister(string path)
: base(path, null) { }
private ChardonnayThemePersister(ChardonnayTheme target, string path)
: base(target, path, null) { }
protected override JsonSerializerSettings GetSerializerSettings()
=> new JsonSerializerSettings { Converters = { new ColorConverter() } };
public static ChardonnayThemePersister? Create()
{
try
{
return System.IO.File.Exists(jsonPath)
? new ChardonnayThemePersister(jsonPath)
: new ChardonnayThemePersister(ChardonnayTheme.GetLiveTheme(), jsonPath);
}
catch (Exception ex)
{
try
{
Serilog.Log.Logger.Error(ex, $"Failed to open {jsonPath}. Overwriting with empty file.");
FileUtility.SaferDelete(jsonPath);
return new ChardonnayThemePersister(ChardonnayTheme.GetLiveTheme(), jsonPath);
}
catch (Exception ex2)
{
Serilog.Log.Logger.Error(ex2, $"Failed to open {jsonPath}.");
return null;
}
}
}
/// <summary> Store colors as #ARGB values so that the json file is easier to manually edit </summary>
private class ColorConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(Color);
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
=> reader.Value is string hexColor && Color.TryParse(hexColor, out var color) ? color : default;
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is Color color)
writer.WriteValue(color.ToString());
}
}
}

View File

@ -1,4 +1,3 @@
using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using DataLayer; using DataLayer;
using LibationUiBase.GridView; using LibationUiBase.GridView;
@ -9,8 +8,6 @@ namespace LibationAvalonia.ViewModels
{ {
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
{ {
public override IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;
private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { } private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook); public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook);

View File

@ -61,7 +61,7 @@ namespace LibationAvalonia.ViewModels
#region Properties exposed to the view #region Properties exposed to the view
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } } public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } } public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
public string? Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); } public string? Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
public string? Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); } public string? Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
public string? Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); } public string? Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
@ -72,13 +72,6 @@ namespace LibationAvalonia.ViewModels
public bool IsDownloading => Status is ProcessBookStatus.Working; public bool IsDownloading => Status is ProcessBookStatus.Working;
public bool Queued => Status is ProcessBookStatus.Queued; public bool Queued => Status is ProcessBookStatus.Queued;
public IBrush BackgroundColor => Status switch
{
ProcessBookStatus.Cancelled => App.ProcessQueueBookCancelledBrush,
ProcessBookStatus.Completed => App.ProcessQueueBookCompletedBrush,
ProcessBookStatus.Failed => App.ProcessQueueBookFailedBrush,
_ => App.ProcessQueueBookDefaultBrush,
};
public string StatusText => Result switch public string StatusText => Result switch
{ {
ProcessBookResult.Success => "Finished", ProcessBookResult.Success => "Finished",

View File

@ -45,7 +45,6 @@ namespace LibationAvalonia.ViewModels.Settings
config.CreationTime = CreationTime.Value; config.CreationTime = CreationTime.Value;
config.LastWriteTime = LastWriteTime.Value; config.LastWriteTime = LastWriteTime.Value;
config.LogLevel = LoggingLevel; config.LogLevel = LoggingLevel;
Configuration.Instance.SetString(ThemeVariant, nameof(ThemeVariant));
} }
private static float scaleFactorToLinearRange(float scaleFactor) private static float scaleFactorToLinearRange(float scaleFactor)
@ -95,20 +94,7 @@ namespace LibationAvalonia.ViewModels.Settings
public string ThemeVariant public string ThemeVariant
{ {
get => themeVariant; get => themeVariant;
set set => this.RaiseAndSetIfChanged(ref themeVariant, value);
{
var changed = !value.Equals(themeVariant);
this.RaiseAndSetIfChanged(ref themeVariant, value);
if (changed && App.Current is Avalonia.Application app)
app.RequestedThemeVariant = themeVariant switch
{
nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
// "System"
_ => Avalonia.Styling.ThemeVariant.Default
};
}
} }
} }
} }

View File

@ -26,8 +26,8 @@
</Style> </Style>
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter"> <Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="{StaticResource SystemChromeDisabledLowColor}" />
<Setter Property="BorderBrush" Value="Transparent" /> <Setter Property="BorderBrush" Value="{StaticResource SystemChromeDisabledLowColor}" />
</Style> </Style>
</UserControl.Styles> </UserControl.Styles>
@ -72,12 +72,6 @@
IsVisible="{CompiledBinding IsError}" IsVisible="{CompiledBinding IsError}"
Fill="{DynamicResource CancelRed}" Fill="{DynamicResource CancelRed}"
Data="{StaticResource BookErrorIcon}" /> Data="{StaticResource BookErrorIcon}" />
<Path
Stretch="Fill"
IsVisible="{CompiledBinding !IsButtonEnabled}"
Fill="{DynamicResource DisabledGrayBrush}"
Data="M0,0 H1 V1 H0" />
</Panel> </Panel>
</Viewbox> </Viewbox>
</Grid> </Grid>

View File

@ -3,9 +3,10 @@
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:vm="clr-namespace:LibationAvalonia.ViewModels" xmlns:vm="clr-namespace:LibationAvalonia.ViewModels"
xmlns:views="clr-namespace:LibationAvalonia.Views"
x:DataType="vm:ProcessBookViewModel" x:DataType="vm:ProcessBookViewModel"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"
x:Class="LibationAvalonia.Views.ProcessBookControl" Background="{CompiledBinding BackgroundColor}"> x:Class="LibationAvalonia.Views.ProcessBookControl">
<UserControl.Styles> <UserControl.Styles>
<Style Selector="Border#QueuedItemBorder:not(:pointerover) Button"> <Style Selector="Border#QueuedItemBorder:not(:pointerover) Button">
@ -14,6 +15,18 @@
<Style Selector="Border#QueuedItemBorder:pointerover Button"> <Style Selector="Border#QueuedItemBorder:pointerover Button">
<Setter Property="IsVisible" Value="True" /> <Setter Property="IsVisible" Value="True" />
</Style> </Style>
<Style Selector="views|ProcessBookControl">
<Setter Property="ProcessBookStatus" Value="{CompiledBinding Status}" />
<Style Selector="^[ProcessBookStatus=Cancelled]">
<Setter Property="Background" Value="{DynamicResource ProcessQueueBookCancelledBrush}" />
</Style>
<Style Selector="^[ProcessBookStatus=Failed]">
<Setter Property="Background" Value="{DynamicResource ProcessQueueBookFailedBrush}" />
</Style>
<Style Selector="^[ProcessBookStatus=Completed]">
<Setter Property="Background" Value="{DynamicResource ProcessQueueBookCompletedBrush}" />
</Style>
</Style>
</UserControl.Styles> </UserControl.Styles>
<Border Name="QueuedItemBorder" Background="Transparent" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1"> <Border Name="QueuedItemBorder" Background="Transparent" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1">
@ -68,7 +81,7 @@
</StackPanel> </StackPanel>
<Panel Margin="3,0,0,0" Grid.Column="1" VerticalAlignment="Top" IsVisible="{CompiledBinding !IsFinished}"> <Panel Margin="3,0,0,0" Grid.Column="1" VerticalAlignment="Top" IsVisible="{CompiledBinding !IsFinished}">
<Button Height="32" Background="{DynamicResource CancelRed}" Width="22" CornerRadius="11" Click="Cancel_Click"> <Button Height="32" Background="{DynamicResource CancelRed}" Width="22" CornerRadius="11" Click="Cancel_Click">
<Path Fill="{DynamicResource ProcessQueueBookDefaultBrush}" VerticalAlignment="Center" Data="{StaticResource CancelButtonIcon}" RenderTransform="{StaticResource Rotate45Transform}" /> <Path Fill="{DynamicResource SystemAltHighColor}" VerticalAlignment="Center" Data="{StaticResource CancelButtonIcon}" RenderTransform="{StaticResource Rotate45Transform}" />
</Button> </Button>
</Panel> </Panel>
</Grid> </Grid>

View File

@ -1,4 +1,5 @@
using ApplicationServices; using ApplicationServices;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using DataLayer; using DataLayer;
using LibationAvalonia.ViewModels; using LibationAvalonia.ViewModels;
@ -12,6 +13,16 @@ namespace LibationAvalonia.Views
{ {
public static event QueueItemPositionButtonClicked PositionButtonClicked; public static event QueueItemPositionButtonClicked PositionButtonClicked;
public static event QueueItemCancelButtonClicked CancelButtonClicked; public static event QueueItemCancelButtonClicked CancelButtonClicked;
public static readonly StyledProperty<ProcessBookStatus> ProcessBookStatusProperty =
AvaloniaProperty.Register<ProcessBookControl, ProcessBookStatus>(nameof(ProcessBookStatus), enableDataValidation: true);
public ProcessBookStatus ProcessBookStatus
{
get => GetValue(ProcessBookStatusProperty);
set => SetValue(ProcessBookStatusProperty, value);
}
public ProcessBookControl() public ProcessBookControl()
{ {
InitializeComponent(); InitializeComponent();
@ -23,6 +34,7 @@ namespace LibationAvalonia.Views
context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
LogMe.RegisterForm(default(ILogForm)) LogMe.RegisterForm(default(ILogForm))
); );
return; return;
} }
} }

View File

@ -35,7 +35,7 @@
<TextBlock FontSize="14" VerticalAlignment="Center">Process Queue</TextBlock> <TextBlock FontSize="14" VerticalAlignment="Center">Process Queue</TextBlock>
</TabItem.Header> </TabItem.Header>
<Grid ColumnDefinitions="*" RowDefinitions="*,40"> <Grid ColumnDefinitions="*" RowDefinitions="*,40">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemChromeMediumLowColor}"> <Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemRegionColor}">
<ScrollViewer <ScrollViewer
Name="scroller" Name="scroller"
HorizontalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled"
@ -81,7 +81,7 @@
<TextBlock FontSize="14" VerticalAlignment="Center">Queue Log</TextBlock> <TextBlock FontSize="14" VerticalAlignment="Center">Queue Log</TextBlock>
</TabItem.Header> </TabItem.Header>
<Grid ColumnDefinitions="*" RowDefinitions="*,40"> <Grid ColumnDefinitions="*" RowDefinitions="*,40">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemChromeMediumLowColor}"> <Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemRegionColor}">
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding LogEntries}"> <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding LogEntries}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn SortMemberPath="LogDate" Header="Timestamp" CanUserSort="True" Binding="{Binding LogDateString}" Width="90"/> <DataGridTextColumn SortMemberPath="LogDate" Header="Timestamp" CanUserSort="True" Binding="{Binding LogDateString}" Width="90"/>

View File

@ -9,13 +9,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Views namespace LibationAvalonia.Views
{ {
public partial class ProcessQueueControl : UserControl public partial class ProcessQueueControl : UserControl
{ {
private TrackedQueue<ProcessBookViewModel> Queue => _viewModel.Queue; private TrackedQueue<ProcessBookViewModel>? Queue => _viewModel?.Queue;
private ProcessQueueViewModel _viewModel => DataContext as ProcessQueueViewModel; private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
public ProcessQueueControl() public ProcessQueueControl()
{ {
@ -25,6 +27,7 @@ namespace LibationAvalonia.Views
ProcessBookControl.CancelButtonClicked += ProcessBookControl2_CancelButtonClicked; ProcessBookControl.CancelButtonClicked += ProcessBookControl2_CancelButtonClicked;
#region Design Mode Testing #region Design Mode Testing
#if DEBUG
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
var vm = new ProcessQueueViewModel(); var vm = new ProcessQueueViewModel();
@ -85,6 +88,7 @@ namespace LibationAvalonia.Views
vm.Queue.MoveNext(); vm.Queue.MoveNext();
return; return;
} }
#endif
#endregion #endregion
} }
@ -98,53 +102,59 @@ namespace LibationAvalonia.Views
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item) private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item)
{ {
if (item is not null) if (item is not null)
{
await item.CancelAsync(); await item.CancelAsync();
Queue.RemoveQueued(item); Queue?.RemoveQueued(item);
}
} }
private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel item, QueuePosition queueButton) private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel item, QueuePosition queueButton)
{ {
Queue.MoveQueuePosition(item, queueButton); Queue?.MoveQueuePosition(item, queueButton);
} }
public async void CancelAllBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void CancelAllBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
Queue.ClearQueue(); Queue?.ClearQueue();
if (Queue.Current is not null) if (Queue?.Current is not null)
await Queue.Current.CancelAsync(); await Queue.Current.CancelAsync();
} }
public void ClearFinishedBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void ClearFinishedBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
Queue.ClearCompleted(); Queue?.ClearCompleted();
if (!_viewModel.Running) if (_viewModel?.Running is false)
_viewModel.RunningTime = string.Empty; _viewModel.RunningTime = string.Empty;
} }
public void ClearLogBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void ClearLogBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
_viewModel.LogEntries.Clear(); _viewModel?.LogEntries.Clear();
} }
private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
string logText = string.Join("\r\n", _viewModel.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}")); if (_viewModel is ProcessQueueViewModel vm)
await App.MainWindow.Clipboard.SetTextAsync(logText); {
string logText = string.Join("\r\n", vm.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}"));
if (App.MainWindow?.Clipboard?.SetTextAsync(logText) is Task setter)
await setter;
}
} }
private async void cancelAllBtn_Click(object sender, EventArgs e) private async void cancelAllBtn_Click(object sender, EventArgs e)
{ {
Queue.ClearQueue(); Queue?.ClearQueue();
if (Queue.Current is not null) if (Queue?.Current is not null)
await Queue.Current.CancelAsync(); await Queue.Current.CancelAsync();
} }
private void btnClearFinished_Click(object sender, EventArgs e) private void btnClearFinished_Click(object sender, EventArgs e)
{ {
Queue.ClearCompleted(); Queue?.ClearCompleted();
if (!_viewModel.Running) if (_viewModel?.Running is false)
_viewModel.RunningTime = string.Empty; _viewModel.RunningTime = string.Empty;
} }
@ -155,7 +165,7 @@ namespace LibationAvalonia.Views
{ {
public static readonly DecimalConverter Instance = new(); public static readonly DecimalConverter Instance = new();
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 sourceText && targetType.IsAssignableTo(typeof(decimal?))) if (value is string sourceText && targetType.IsAssignableTo(typeof(decimal?)))
{ {
@ -172,7 +182,7 @@ namespace LibationAvalonia.Views
return 0; return 0;
} }
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 decimal val) if (value is decimal val)
{ {
@ -184,7 +194,7 @@ namespace LibationAvalonia.Views
: val.ToString("F2") : val.ToString("F2")
) + " MB/s"; ) + " MB/s";
} }
return value.ToString(); return value?.ToString();
} }
} }
} }

View File

@ -18,6 +18,7 @@
ItemsSource="{Binding GridEntries}" ItemsSource="{Binding GridEntries}"
CanUserSortColumns="True" BorderThickness="3" CanUserSortColumns="True" BorderThickness="3"
CanUserResizeColumns="True" CanUserResizeColumns="True"
LoadingRow="ProductsDisplay_LoadingRow"
CanUserReorderColumns="True"> CanUserReorderColumns="True">
<DataGrid.Styles> <DataGrid.Styles>
@ -93,7 +94,7 @@
<controls:DataGridTemplateColumnExt Header="Title" MinWidth="10" Width="{Binding TitleWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}"> <controls:DataGridTemplateColumnExt Header="Title" MinWidth="10" Width="{Binding TitleWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}"> <Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Classes="h1" Text="{CompiledBinding Title}" /> <TextBlock Classes="h1" Text="{CompiledBinding Title}" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -103,7 +104,7 @@
<controls:DataGridTemplateColumnExt Header="Authors" MinWidth="10" Width="{Binding AuthorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Authors" ClipboardContentBinding="{Binding Authors}"> <controls:DataGridTemplateColumnExt Header="Authors" MinWidth="10" Width="{Binding AuthorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Authors" ClipboardContentBinding="{Binding Authors}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}"> <Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Authors}" /> <TextBlock Text="{CompiledBinding Authors}" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -113,7 +114,7 @@
<controls:DataGridTemplateColumnExt Header="Narrators" MinWidth="10" Width="{Binding NarratorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Narrators" ClipboardContentBinding="{Binding Narrators}"> <controls:DataGridTemplateColumnExt Header="Narrators" MinWidth="10" Width="{Binding NarratorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Narrators" ClipboardContentBinding="{Binding Narrators}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}"> <Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Narrators}" /> <TextBlock Text="{CompiledBinding Narrators}" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -123,7 +124,7 @@
<controls:DataGridTemplateColumnExt Header="Length" MinWidth="10" Width="{Binding LengthWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}"> <controls:DataGridTemplateColumnExt Header="Length" MinWidth="10" Width="{Binding LengthWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}"> <Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Length}" /> <TextBlock Text="{CompiledBinding Length}" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -133,7 +134,7 @@
<controls:DataGridTemplateColumnExt Header="Series" MinWidth="10" Width="{Binding SeriesWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Series" ClipboardContentBinding="{Binding Series}"> <controls:DataGridTemplateColumnExt Header="Series" MinWidth="10" Width="{Binding SeriesWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Series" ClipboardContentBinding="{Binding Series}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}"> <Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Series}" /> <TextBlock Text="{CompiledBinding Series}" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -143,7 +144,7 @@
<controls:DataGridTemplateColumnExt Header="Series&#xA;Order" MinWidth="10" Width="{Binding SeriesOrderWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="SeriesOrder" ClipboardContentBinding="{Binding Series}"> <controls:DataGridTemplateColumnExt Header="Series&#xA;Order" MinWidth="10" Width="{Binding SeriesOrderWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="SeriesOrder" ClipboardContentBinding="{Binding Series}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}"> <Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding SeriesOrder}" HorizontalAlignment="Center" /> <TextBlock Text="{CompiledBinding SeriesOrder}" HorizontalAlignment="Center" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -153,7 +154,7 @@
<controls:DataGridTemplateColumnExt Header="Description" MinWidth="10" Width="{Binding DescriptionWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding Description}"> <controls:DataGridTemplateColumnExt Header="Description" MinWidth="10" Width="{Binding DescriptionWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding Description}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" > <Panel Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
<TextBlock Text="{CompiledBinding Description}" VerticalAlignment="Top" /> <TextBlock Text="{CompiledBinding Description}" VerticalAlignment="Top" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -163,7 +164,7 @@
<controls:DataGridTemplateColumnExt Header="Category" MinWidth="10" Width="{Binding CategoryWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}"> <controls:DataGridTemplateColumnExt Header="Category" MinWidth="10" Width="{Binding CategoryWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}"> <Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Category}" /> <TextBlock Text="{CompiledBinding Category}" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -177,14 +178,13 @@
MinWidth="10" Width="{Binding ProductRatingWidth, Mode=TwoWay}" MinWidth="10" Width="{Binding ProductRatingWidth, Mode=TwoWay}"
SortMemberPath="ProductRating" CanUserSort="True" SortMemberPath="ProductRating" CanUserSort="True"
OpacityBinding="{CompiledBinding Liberate.Opacity}" OpacityBinding="{CompiledBinding Liberate.Opacity}"
BackgroundBinding="{CompiledBinding Liberate.BackgroundBrush}"
ClipboardContentBinding="{CompiledBinding ProductRating}" ClipboardContentBinding="{CompiledBinding ProductRating}"
Binding="{CompiledBinding ProductRating}" /> Binding="{CompiledBinding ProductRating}" />
<controls:DataGridTemplateColumnExt Header="Purchase&#xA;Date" MinWidth="10" Width="{Binding PurchaseDateWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="PurchaseDate" ClipboardContentBinding="{Binding PurchaseDate}"> <controls:DataGridTemplateColumnExt Header="Purchase&#xA;Date" MinWidth="10" Width="{Binding PurchaseDateWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="PurchaseDate" ClipboardContentBinding="{Binding PurchaseDate}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}"> <Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding PurchaseDate}" /> <TextBlock Text="{CompiledBinding PurchaseDate}" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -198,14 +198,13 @@
MinWidth="10" Width="{Binding MyRatingWidth, Mode=TwoWay}" MinWidth="10" Width="{Binding MyRatingWidth, Mode=TwoWay}"
SortMemberPath="MyRating" CanUserSort="True" SortMemberPath="MyRating" CanUserSort="True"
OpacityBinding="{CompiledBinding Liberate.Opacity}" OpacityBinding="{CompiledBinding Liberate.Opacity}"
BackgroundBinding="{CompiledBinding Liberate.BackgroundBrush}"
ClipboardContentBinding="{CompiledBinding MyRating}" ClipboardContentBinding="{CompiledBinding MyRating}"
Binding="{CompiledBinding MyRating, Mode=TwoWay}" /> Binding="{CompiledBinding MyRating, Mode=TwoWay}" />
<controls:DataGridTemplateColumnExt Header="Misc" MinWidth="10" Width="{Binding MiscWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}"> <controls:DataGridTemplateColumnExt Header="Misc" MinWidth="10" Width="{Binding MiscWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}"> <Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Misc}" TextWrapping="WrapWithOverflow" /> <TextBlock Text="{CompiledBinding Misc}" TextWrapping="WrapWithOverflow" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>
@ -215,7 +214,7 @@
<controls:DataGridTemplateColumnExt Header="Last&#xA;Download" MinWidth="10" Width="{Binding LastDownloadWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="LastDownload" ClipboardContentBinding="{Binding LastDownload}"> <controls:DataGridTemplateColumnExt Header="Last&#xA;Download" MinWidth="10" Width="{Binding LastDownloadWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="LastDownload" ClipboardContentBinding="{Binding LastDownload}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry"> <DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick"> <Panel Opacity="{CompiledBinding Liberate.Opacity}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick">
<TextBlock Text="{CompiledBinding LastDownload}" TextWrapping="WrapWithOverflow" /> <TextBlock Text="{CompiledBinding LastDownload}" TextWrapping="WrapWithOverflow" />
</Panel> </Panel>
</DataTemplate> </DataTemplate>

View File

@ -1,6 +1,8 @@
using ApplicationServices; using ApplicationServices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Styling; using Avalonia.Styling;
using DataLayer; using DataLayer;
@ -17,16 +19,17 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Views namespace LibationAvalonia.Views
{ {
public partial class ProductsDisplay : UserControl public partial class ProductsDisplay : UserControl
{ {
public event EventHandler<LibraryBook> LiberateClicked; public event EventHandler<LibraryBook>? LiberateClicked;
public event EventHandler<ISeriesEntry> LiberateSeriesClicked; public event EventHandler<ISeriesEntry>? LiberateSeriesClicked;
public event EventHandler<LibraryBook> ConvertToMp3Clicked; public event EventHandler<LibraryBook>? ConvertToMp3Clicked;
private ProductsDisplayViewModel _viewModel => DataContext as ProductsDisplayViewModel; private ProductsDisplayViewModel? _viewModel => DataContext as ProductsDisplayViewModel;
ImageDisplayDialog imageDisplayDialog; ImageDisplayDialog? imageDisplayDialog;
public ProductsDisplay() public ProductsDisplay()
{ {
@ -52,6 +55,8 @@ namespace LibationAvalonia.Views
Configuration.Instance.PropertyChanged += Configuration_GridScaleChanged; Configuration.Instance.PropertyChanged += Configuration_GridScaleChanged;
Configuration.Instance.PropertyChanged += Configuration_FontChanged; Configuration.Instance.PropertyChanged += Configuration_FontChanged;
#region Design Mode Testing
#if DEBUG
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
@ -80,6 +85,8 @@ namespace LibationAvalonia.Views
setFontScale(1); setFontScale(1);
return; return;
} }
#endif
#endregion
setGridScale(Configuration.Instance.GridScaleFactor); setGridScale(Configuration.Instance.GridScaleFactor);
setFontScale(Configuration.Instance.GridFontScaleFactor); setFontScale(Configuration.Instance.GridFontScaleFactor);
@ -91,6 +98,14 @@ namespace LibationAvalonia.Views
} }
} }
private void ProductsDisplay_LoadingRow(object sender, DataGridRowEventArgs e)
{
if (e.Row.DataContext is LibraryBookEntry<AvaloniaEntryStatus> entry && entry.Liberate.IsEpisode)
e.Row.DynamicResource(DataGridRow.BackgroundProperty, "SeriesEntryGridBackgroundBrush");
else
e.Row.Background = Brushes.Transparent;
}
private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{ {
if (sender is DataGridColumn col && e.Property == DataGridColumn.IsVisibleProperty) if (sender is DataGridColumn col && e.Property == DataGridColumn.IsVisibleProperty)
@ -105,13 +120,15 @@ namespace LibationAvalonia.Views
[PropertyChangeFilter(nameof(Configuration.GridScaleFactor))] [PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
private void Configuration_GridScaleChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e) private void Configuration_GridScaleChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
{ {
setGridScale((float)e.NewValue); if (e.NewValue is float value)
setGridScale(value);
} }
[PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))] [PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
private void Configuration_FontChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e) private void Configuration_FontChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
{ {
setFontScale((float)e.NewValue); if (e.NewValue is float value)
setFontScale(value);
} }
private readonly Style rowHeightStyle; private readonly Style rowHeightStyle;
@ -171,17 +188,18 @@ namespace LibationAvalonia.Views
#region Cell Context Menu #region Cell Context Menu
public void ProductsGrid_CellContextMenuStripNeeded(object sender, DataGridCellContextMenuStripNeededEventArgs args) public void ProductsGrid_CellContextMenuStripNeeded(object? sender, DataGridCellContextMenuStripNeededEventArgs args)
{ {
var entry = args.GridEntry; var entry = args.GridEntry;
var ctx = new GridContextMenu(entry, '_'); var ctx = new GridContextMenu(entry, '_');
if (args.Column.SortMemberPath is not "Liberate" and not "Cover") if (args.Column.SortMemberPath is not "Liberate" and not "Cover"
&& App.MainWindow?.Clipboard is IClipboard clipboard)
{ {
args.ContextMenuItems.Add(new MenuItem args.ContextMenuItems.Add(new MenuItem
{ {
Header = ctx.CopyCellText, Header = ctx.CopyCellText,
Command = ReactiveCommand.CreateFromTask(() => App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents)) Command = ReactiveCommand.CreateFromTask(() => clipboard?.SetTextAsync(args.CellClipboardContents) ?? Task.CompletedTask)
}); });
args.ContextMenuItems.Add(new Separator()); args.ContextMenuItems.Add(new Separator());
} }
@ -240,13 +258,14 @@ namespace LibationAvalonia.Views
{ {
try try
{ {
var window = this.GetParentWindow(); if (this.GetParentWindow() is not Window window)
return;
var openFileDialogOptions = new FilePickerOpenOptions var openFileDialogOptions = new FilePickerOpenOptions
{ {
Title = ctx.LocateFileDialogTitle, Title = ctx.LocateFileDialogTitle,
AllowMultiple = false, AllowMultiple = false,
SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix), SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix!),
FileTypeFilter = new FilePickerFileType[] FileTypeFilter = new FilePickerFileType[]
{ {
new("All files (*.*)") { Patterns = new[] { "*" } }, new("All files (*.*)") { Patterns = new[] { "*" } },
@ -303,7 +322,7 @@ namespace LibationAvalonia.Views
{ {
var template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate); var template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate);
var form = new EditTemplateDialog(template); var form = new EditTemplateDialog(template);
if (await form.ShowDialog<DialogResult>(this.GetParentWindow()) == DialogResult.OK) if (this.GetParentWindow() is Window window && await form.ShowDialog<DialogResult>(window) == DialogResult.OK)
{ {
setNewTemplate(template.EditingTemplate.TemplateText); setNewTemplate(template.EditingTemplate.TemplateText);
} }
@ -340,12 +359,12 @@ namespace LibationAvalonia.Views
#region View Bookmarks/Clips #region View Bookmarks/Clips
if (!entry.Liberate.IsSeries) if (!entry.Liberate.IsSeries && VisualRoot is Window window)
{ {
args.ContextMenuItems.Add(new MenuItem args.ContextMenuItems.Add(new MenuItem
{ {
Header = ctx.ViewBookmarksText, Header = ctx.ViewBookmarksText,
Command = ReactiveCommand.CreateFromTask(() => new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window)) Command = ReactiveCommand.CreateFromTask(() => new BookRecordsDialog(entry.LibraryBook).ShowDialog(window))
}); });
} }
@ -389,6 +408,9 @@ namespace LibationAvalonia.Views
var HeaderCell_PI = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var HeaderCell_PI = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (HeaderCell_PI is null)
return;
foreach (var column in productsGrid.Columns) foreach (var column in productsGrid.Columns)
{ {
var itemName = column.SortMemberPath; var itemName = column.SortMemberPath;
@ -406,8 +428,9 @@ namespace LibationAvalonia.Views
} }
); );
var headercell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader; var headerCell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
headercell.ContextMenu = contextMenu; if (headerCell is not null)
headerCell.ContextMenu = contextMenu;
column.IsVisible = gridColumnsVisibilities.GetValueOrDefault(itemName, true); column.IsVisible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
} }
@ -425,30 +448,30 @@ namespace LibationAvalonia.Views
} }
} }
private void ContextMenu_ContextMenuOpening(object sender, System.ComponentModel.CancelEventArgs e) private void ContextMenu_ContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
{ {
var contextMenu = sender as ContextMenu; if (sender is not ContextMenu contextMenu)
return;
foreach (var mi in contextMenu.Items.OfType<MenuItem>()) foreach (var mi in contextMenu.Items.OfType<MenuItem>())
{ {
if (mi.Tag is DataGridColumn column) if (mi.Tag is DataGridColumn column && mi.Icon is CheckBox cbox)
{ {
var cbox = mi.Icon as CheckBox;
cbox.IsChecked = column.IsVisible; cbox.IsChecked = column.IsVisible;
} }
} }
} }
private void ContextMenu_MenuClosed(object sender, Avalonia.Interactivity.RoutedEventArgs e) private void ContextMenu_MenuClosed(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
var contextMenu = sender as ContextMenu; if (sender is not ContextMenu contextMenu)
return;
var config = Configuration.Instance; var config = Configuration.Instance;
var dictionary = config.GridColumnsVisibilities; var dictionary = config.GridColumnsVisibilities;
foreach (var mi in contextMenu.Items.OfType<MenuItem>()) foreach (var mi in contextMenu.Items.OfType<MenuItem>())
{ {
if (mi.Tag is DataGridColumn column) if (mi.Tag is DataGridColumn column && mi.Icon is CheckBox cbox)
{ {
var cbox = mi.Icon as CheckBox;
column.IsVisible = cbox.IsChecked == true; column.IsVisible = cbox.IsChecked == true;
dictionary[column.SortMemberPath] = cbox.IsChecked == true; dictionary[column.SortMemberPath] = cbox.IsChecked == true;
} }
@ -463,7 +486,7 @@ namespace LibationAvalonia.Views
config.GridColumnsVisibilities = dictionary; config.GridColumnsVisibilities = dictionary;
} }
private void ProductsGrid_ColumnDisplayIndexChanged(object sender, DataGridColumnEventArgs e) private void ProductsGrid_ColumnDisplayIndexChanged(object? sender, DataGridColumnEventArgs e)
{ {
var config = Configuration.Instance; var config = Configuration.Instance;
@ -478,9 +501,10 @@ namespace LibationAvalonia.Views
public async void LiberateButton_Click(object sender, EventArgs e) public async void LiberateButton_Click(object sender, EventArgs e)
{ {
var button = sender as LiberateStatusButton; if (sender is not LiberateStatusButton button)
return;
if (button.DataContext is ISeriesEntry sEntry) if (button.DataContext is ISeriesEntry sEntry && _viewModel is not null)
{ {
await _viewModel.ToggleSeriesExpanded(sEntry); await _viewModel.ToggleSeriesExpanded(sEntry);
@ -518,7 +542,7 @@ namespace LibationAvalonia.Views
var picDef = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native); var picDef = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
void PictureCached(object sender, PictureCachedEventArgs e) void PictureCached(object? sender, PictureCachedEventArgs e)
{ {
if (e.Definition.PictureId == picDef.PictureId) if (e.Definition.PictureId == picDef.PictureId)
imageDisplayDialog.SetCoverBytes(e.Picture); imageDisplayDialog.SetCoverBytes(e.Picture);
@ -558,7 +582,7 @@ namespace LibationAvalonia.Views
DescriptionText = gEntry.Description, DescriptionText = gEntry.Description,
}; };
void CloseWindow(object o, DataGridRowEventArgs e) void CloseWindow(object? o, DataGridRowEventArgs e)
{ {
displayWindow.Close(); displayWindow.Close();
} }
@ -572,13 +596,13 @@ namespace LibationAvalonia.Views
} }
} }
BookDetailsDialog bookDetailsForm; BookDetailsDialog? bookDetailsForm;
public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args) public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{ {
var button = args.Source as Button; var button = args.Source as Button;
if (button.DataContext is ILibraryBookEntry lbEntry && VisualRoot is Window window) if (button?.DataContext is ILibraryBookEntry lbEntry && VisualRoot is Window window)
{ {
if (bookDetailsForm is null || !bookDetailsForm.IsVisible) if (bookDetailsForm is null || !bookDetailsForm.IsVisible)
{ {

View File

@ -61,7 +61,6 @@ namespace LibationUiBase.GridView
|| PdfStatus is not null and not LiberatedStatus.Liberated || PdfStatus is not null and not LiberatedStatus.Liberated
); );
public double Opacity => !IsSeries && Book.UserDefinedItem.Tags.ContainsInsensitive("hidden") ? 0.4 : 1; public double Opacity => !IsSeries && Book.UserDefinedItem.Tags.ContainsInsensitive("hidden") ? 0.4 : 1;
public abstract object BackgroundBrush { get; }
public object ButtonImage => GetLiberateIcon(); public object ButtonImage => GetLiberateIcon();
public string ToolTip => GetTooltip(); public string ToolTip => GetTooltip();
private Book Book { get; } private Book Book { get; }

View File

@ -7,7 +7,7 @@ namespace LibationWinForms.GridView
public class WinFormsEntryStatus : EntryStatus, IEntryStatus public class WinFormsEntryStatus : EntryStatus, IEntryStatus
{ {
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230); private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
public override object BackgroundBrush => IsEpisode ? SERIES_BG_COLOR : SystemColors.ControlLightLight; public Color BackgroundBrush => IsEpisode ? SERIES_BG_COLOR : SystemColors.ControlLightLight;
private WinFormsEntryStatus(LibraryBook libraryBook) : base(libraryBook) { } private WinFormsEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
public static EntryStatus Create(LibraryBook libraryBook) => new WinFormsEntryStatus(libraryBook); public static EntryStatus Create(LibraryBook libraryBook) => new WinFormsEntryStatus(libraryBook);