diff --git a/Source/LibationAvalonia/App.axaml b/Source/LibationAvalonia/App.axaml
index 13a64b07..9edde65f 100644
--- a/Source/LibationAvalonia/App.axaml
+++ b/Source/LibationAvalonia/App.axaml
@@ -20,6 +20,13 @@
+
+
+
+ 0
+
+
+
@@ -28,17 +35,11 @@
-
-
-
-
-
-
@@ -47,29 +48,26 @@
-
-
-
-
-
-
-
-
+
-
-
+
-
+
+
+
+
+
+
@@ -106,7 +104,7 @@
-
+
diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs
index 55e2a137..3201b953 100644
--- a/Source/LibationAvalonia/App.axaml.cs
+++ b/Source/LibationAvalonia/App.axaml.cs
@@ -2,7 +2,6 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
-using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling;
using LibationAvalonia.Dialogs;
@@ -13,19 +12,22 @@ using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Threading;
+using Dinah.Core;
+using LibationAvalonia.Themes;
+using Avalonia.Data.Core.Plugins;
+using System.Linq;
+#nullable enable
namespace LibationAvalonia
{
public class App : Application
{
- public static MainWindow MainWindow { get; private set; }
- public static IBrush ProcessQueueBookFailedBrush { get; private set; }
- public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
- public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
- public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
- public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
+ public static Task>? LibraryTask { get; set; }
+ public static ChardonnayTheme? DefaultThemeColors { get; private set; }
+ public static MainWindow? MainWindow { get; private set; }
+ public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/");
+ public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet.");
- public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
@@ -34,12 +36,16 @@ namespace LibationAvalonia
AvaloniaXamlLoader.Load(this);
}
- public static Task> LibraryTask;
-
public override void OnFrameworkInitializationCompleted()
{
+ DefaultThemeColors = ChardonnayTheme.GetLiveTheme();
+
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;
if (!config.LibationSettingsAreValid)
@@ -69,11 +75,23 @@ namespace LibationAvalonia
base.OnFrameworkInitializationCompleted();
}
-
- private async void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+ private void DisableAvaloniaDataAnnotationValidation()
{
- var setupDialog = sender as SetupDialog;
- var desktop = ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
+ // Get an array of plugins to remove
+ var dataValidationPluginsToRemove =
+ BindingPlugins.DataValidators.OfType().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
{
@@ -87,7 +105,7 @@ namespace LibationAvalonia
if (setupDialog.Config.LibationSettingsAreValid)
{
- string theme = setupDialog.SelectedTheme.Content as string;
+ string? theme = setupDialog.SelectedTheme.Content as string;
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
@@ -143,7 +161,7 @@ namespace LibationAvalonia
desktop.MainWindow = libationFilesDialog;
libationFilesDialog.Show();
- void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
+ void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e)
{
libationFilesDialog.Closing -= WindowClosing;
e.Cancel = true;
@@ -201,16 +219,9 @@ namespace LibationAvalonia
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
{
- Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) switch
- {
- nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
- nameof(ThemeVariant.Light) => ThemeVariant.Light,
- // "System"
- _ => ThemeVariant.Default
- };
+ Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
+ OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
- //Reload colors for current theme
- LoadStyles();
var mainWindow = new MainWindow();
desktop.MainWindow = MainWindow = mainWindow;
mainWindow.Loaded += MainWindow_Loaded;
@@ -218,19 +229,23 @@ namespace LibationAvalonia
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;
- await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
+ using var themePersister = ChardonnayThemePersister.Create();
+ 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));
- ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCompletedBrush));
- ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCancelledBrush));
- SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources(nameof(SeriesEntryGridBackgroundBrush));
- ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookDefaultBrush));
+ if (LibraryTask is not null && MainWindow is not null)
+ {
+ var library = await LibraryTask;
+ await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
+ }
}
}
}
diff --git a/Source/LibationAvalonia/Assets/DataGridColumnHeader.xaml b/Source/LibationAvalonia/Assets/DataGridColumnHeader.xaml
deleted file mode 100644
index 8c82dc2d..00000000
--- a/Source/LibationAvalonia/Assets/DataGridColumnHeader.xaml
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/LibationAvalonia/AvaloniaUtils.cs b/Source/LibationAvalonia/AvaloniaUtils.cs
index 714491bf..11508b70 100644
--- a/Source/LibationAvalonia/AvaloniaUtils.cs
+++ b/Source/LibationAvalonia/AvaloniaUtils.cs
@@ -1,8 +1,8 @@
-using Avalonia.Controls;
-using Avalonia.Media;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media.Imaging;
using Avalonia.VisualTree;
-using LibationAvalonia.Dialogs;
using LibationFileManager;
using System.Threading.Tasks;
@@ -11,17 +11,21 @@ namespace LibationAvalonia
{
internal static class AvaloniaUtils
{
- public static IBrush GetBrushFromResources(string name)
- => GetBrushFromResources(name, Brushes.Transparent);
- public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
+ public static T DynamicResource(this T control, AvaloniaProperty prop, object resourceKey) where T : Control
{
- if ((App.Current?.TryGetResource(name, App.Current.ActualThemeVariant, out var value) ?? false) && value is IBrush brush)
- return brush;
- return defaultBrush;
+ control[!prop] = new DynamicResourceExtension(resourceKey);
+ return control;
}
- public static Task ShowDialogAsync(this DialogWindow dialogWindow, Window? owner = null)
- => dialogWindow.ShowDialog(owner ?? App.MainWindow);
+ public static Task ShowDialogAsync(this Dialogs.DialogWindow dialogWindow, Window? owner = null)
+ => ((owner ?? App.MainWindow) is Window window)
+ ? dialogWindow.ShowDialog(window)
+ : Task.FromResult(DialogResult.None);
+
+ public static Task ShowDialogAsync(this Dialogs.Login.WebLoginDialog dialogWindow, Window? owner = null)
+ => ((owner ?? App.MainWindow) is Window window)
+ ? dialogWindow.ShowDialog(window)
+ : Task.FromResult(DialogResult.None);
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
diff --git a/Source/LibationAvalonia/Controls/GroupBox.axaml b/Source/LibationAvalonia/Controls/GroupBox.axaml
index da3b0e32..c73a52bf 100644
--- a/Source/LibationAvalonia/Controls/GroupBox.axaml
+++ b/Source/LibationAvalonia/Controls/GroupBox.axaml
@@ -28,7 +28,7 @@
diff --git a/Source/LibationAvalonia/Controls/Settings/Important.axaml b/Source/LibationAvalonia/Controls/Settings/Important.axaml
index 6653b82a..7745a507 100644
--- a/Source/LibationAvalonia/Controls/Settings/Important.axaml
+++ b/Source/LibationAvalonia/Controls/Settings/Important.axaml
@@ -158,15 +158,24 @@
+ Text="Theme:"/>
+
diff --git a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
index 4a62a273..03e2f05f 100644
--- a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
+++ b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
@@ -1,13 +1,18 @@
using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
using Dinah.Core;
using FileManager;
+using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Settings;
using LibationFileManager;
+using System.Linq;
+#nullable enable
namespace LibationAvalonia.Controls.Settings
{
public partial class Important : UserControl
{
+ private ImportantSettingsVM? ViewModel => DataContext as ImportantSettingsVM;
public Important()
{
InitializeComponent();
@@ -16,6 +21,42 @@ namespace LibationAvalonia.Controls.Settings
_ = Configuration.Instance.LibationFiles;
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().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)
diff --git a/Source/LibationAvalonia/Dialogs/DialogWindow.cs b/Source/LibationAvalonia/Dialogs/DialogWindow.cs
index a5f7c15e..776c9cab 100644
--- a/Source/LibationAvalonia/Dialogs/DialogWindow.cs
+++ b/Source/LibationAvalonia/Dialogs/DialogWindow.cs
@@ -10,6 +10,8 @@ namespace LibationAvalonia.Dialogs
{
public abstract class DialogWindow : Window
{
+ protected bool CancelOnEscape { get; set; } = true;
+ protected bool SaveOnEnter { get; set; } = true;
public bool SaveAndRestorePosition { get; set; } = true;
public Control ControlToFocusOnShow { get; set; }
protected override Type StyleKeyOverride => typeof(DialogWindow);
@@ -132,9 +134,9 @@ namespace LibationAvalonia.Dialogs
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();
- else if (e.Key == Avalonia.Input.Key.Return)
+ else if (SaveOnEnter && e.Key == Avalonia.Input.Key.Return)
await SaveAndCloseAsync();
}
}
diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
index f90fe55d..52f588f1 100644
--- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
+++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
@@ -1,10 +1,10 @@
using AudibleApi;
using AudibleUtilities;
-using Avalonia.Threading;
using LibationFileManager;
using System;
using System.Threading.Tasks;
+#nullable enable
namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginChoiceEager : ILoginChoiceEager
@@ -23,14 +23,14 @@ namespace LibationAvalonia.Dialogs.Login
LoginCallback = new AvaloniaLoginCallback(_account);
}
- public async Task StartAsync(ChoiceIn choiceIn)
+ public async Task StartAsync(ChoiceIn choiceIn)
{
if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
{
try
{
var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
- if (await weblogin.ShowDialog(App.MainWindow) is DialogResult.OK)
+ if (await weblogin.ShowDialogAsync(App.MainWindow) is DialogResult.OK)
return ChoiceOut.External(weblogin.ResponseUrl);
}
catch (Exception ex)
diff --git a/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml b/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml
new file mode 100644
index 00000000..f4c4a804
--- /dev/null
+++ b/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml.cs
new file mode 100644
index 00000000..2d80a84d
--- /dev/null
+++ b/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml.cs
@@ -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())
+ {
+ 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())
+ {
+ i.SuppressSet = true;
+ i.ThemeColor = theme.GetColor(ActualThemeVariant, i.ThemeItemName);
+ i.SuppressSet = false;
+ }
+ }
+
+ private static IEnumerable 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 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);
+ }
+ }
+ }
+}
diff --git a/Source/LibationAvalonia/LibationAvalonia.csproj b/Source/LibationAvalonia/LibationAvalonia.csproj
index 76d104a1..54449762 100644
--- a/Source/LibationAvalonia/LibationAvalonia.csproj
+++ b/Source/LibationAvalonia/LibationAvalonia.csproj
@@ -37,7 +37,6 @@
-
@@ -74,6 +73,7 @@
+
diff --git a/Source/LibationAvalonia/Themes/ChardonnayTheme.cs b/Source/LibationAvalonia/Themes/ChardonnayTheme.cs
new file mode 100644
index 00000000..53b95de6
--- /dev/null
+++ b/Source/LibationAvalonia/Themes/ChardonnayTheme.cs
@@ -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;
+
+ /// Theme color overrides
+ [JsonProperty]
+ private readonly Dictionary> ThemeColors;
+
+ /// The two theme variants supported by Fluent themes
+ private static readonly FrozenSet FluentVariants = [ThemeVariant.Light, ThemeVariant.Dark];
+
+ /// Reusable color pallets for the two theme variants
+ private static readonly FrozenDictionary ColorPalettes
+ = FluentVariants.ToFrozenDictionary(t => t, _ => new ColorPaletteResources());
+
+ private ChardonnayTheme()
+ {
+ ThemeColors = FluentVariants.ToDictionary(t => t, _ => new Dictionary());
+ }
+
+ /// Invoke
+ 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> colorSelector, Color color)
+ => SetColor(FromVariantName(themeVariant), colorSelector, color);
+
+ public ChardonnayTheme SetColor(ThemeVariant themeVariant, Expression> 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 GetThemeColors(string? themeVariant)
+ => GetThemeColors(FromVariantName(themeVariant));
+
+ public FrozenDictionary 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())
+ {
+ 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().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);
+ }
+
+ }
+
+ /// Get the currently-active theme colors.
+ 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())
+ {
+ 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 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
+ };
+}
diff --git a/Source/LibationAvalonia/Themes/ChardonnayThemePersister.cs b/Source/LibationAvalonia/Themes/ChardonnayThemePersister.cs
new file mode 100644
index 00000000..ffe28e09
--- /dev/null
+++ b/Source/LibationAvalonia/Themes/ChardonnayThemePersister.cs
@@ -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
+{
+ 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;
+ }
+ }
+ }
+
+ /// Store colors as #ARGB values so that the json file is easier to manually edit
+ 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());
+ }
+ }
+}
diff --git a/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs b/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs
index 9045e783..605f2e17 100644
--- a/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs
+++ b/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs
@@ -1,4 +1,3 @@
-using Avalonia.Media;
using Avalonia.Media.Imaging;
using DataLayer;
using LibationUiBase.GridView;
@@ -9,8 +8,6 @@ namespace LibationAvalonia.ViewModels
{
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
{
- public override IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;
-
private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook);
diff --git a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs
index c98239ea..16445922 100644
--- a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs
@@ -61,7 +61,7 @@ namespace LibationAvalonia.ViewModels
#region Properties exposed to the view
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? 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)); }
@@ -72,13 +72,6 @@ namespace LibationAvalonia.ViewModels
public bool IsDownloading => Status is ProcessBookStatus.Working;
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
{
ProcessBookResult.Success => "Finished",
diff --git a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
index da6ebada..6b53d6fc 100644
--- a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
+++ b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
@@ -45,7 +45,6 @@ namespace LibationAvalonia.ViewModels.Settings
config.CreationTime = CreationTime.Value;
config.LastWriteTime = LastWriteTime.Value;
config.LogLevel = LoggingLevel;
- Configuration.Instance.SetString(ThemeVariant, nameof(ThemeVariant));
}
private static float scaleFactorToLinearRange(float scaleFactor)
@@ -95,20 +94,7 @@ namespace LibationAvalonia.ViewModels.Settings
public string ThemeVariant
{
get => themeVariant;
- set
- {
- 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
- };
- }
+ set => this.RaiseAndSetIfChanged(ref themeVariant, value);
}
}
}
diff --git a/Source/LibationAvalonia/Views/LiberateStatusButton.axaml b/Source/LibationAvalonia/Views/LiberateStatusButton.axaml
index 2e2db36e..291ec70c 100644
--- a/Source/LibationAvalonia/Views/LiberateStatusButton.axaml
+++ b/Source/LibationAvalonia/Views/LiberateStatusButton.axaml
@@ -26,8 +26,8 @@
@@ -66,18 +66,12 @@
-
+
-
-
diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml b/Source/LibationAvalonia/Views/ProcessBookControl.axaml
index fc227280..864ce3ee 100644
--- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml
+++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml
@@ -3,9 +3,10 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels"
+ xmlns:views="clr-namespace:LibationAvalonia.Views"
x:DataType="vm:ProcessBookViewModel"
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">
+
+
+
+
@@ -68,7 +81,7 @@
diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
index f7ccc4d0..a1e43395 100644
--- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
@@ -1,4 +1,5 @@
using ApplicationServices;
+using Avalonia;
using Avalonia.Controls;
using DataLayer;
using LibationAvalonia.ViewModels;
@@ -12,6 +13,16 @@ namespace LibationAvalonia.Views
{
public static event QueueItemPositionButtonClicked PositionButtonClicked;
public static event QueueItemCancelButtonClicked CancelButtonClicked;
+
+ public static readonly StyledProperty ProcessBookStatusProperty =
+ AvaloniaProperty.Register(nameof(ProcessBookStatus), enableDataValidation: true);
+
+ public ProcessBookStatus ProcessBookStatus
+ {
+ get => GetValue(ProcessBookStatusProperty);
+ set => SetValue(ProcessBookStatusProperty, value);
+ }
+
public ProcessBookControl()
{
InitializeComponent();
@@ -23,6 +34,7 @@ namespace LibationAvalonia.Views
context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
LogMe.RegisterForm(default(ILogForm))
);
+
return;
}
}
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
index 8c3f3815..3691b2e4 100644
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
+++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
@@ -35,7 +35,7 @@
Process Queue
-
+
Queue Log
-
+
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
index 0b7761d8..de001556 100644
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
@@ -9,13 +9,15 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Threading.Tasks;
+#nullable enable
namespace LibationAvalonia.Views
{
public partial class ProcessQueueControl : UserControl
{
- private TrackedQueue Queue => _viewModel.Queue;
- private ProcessQueueViewModel _viewModel => DataContext as ProcessQueueViewModel;
+ private TrackedQueue? Queue => _viewModel?.Queue;
+ private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
public ProcessQueueControl()
{
@@ -25,6 +27,7 @@ namespace LibationAvalonia.Views
ProcessBookControl.CancelButtonClicked += ProcessBookControl2_CancelButtonClicked;
#region Design Mode Testing
+#if DEBUG
if (Design.IsDesignMode)
{
var vm = new ProcessQueueViewModel();
@@ -85,6 +88,7 @@ namespace LibationAvalonia.Views
vm.Queue.MoveNext();
return;
}
+#endif
#endregion
}
@@ -98,53 +102,59 @@ namespace LibationAvalonia.Views
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item)
{
if (item is not null)
+ {
await item.CancelAsync();
- Queue.RemoveQueued(item);
+ Queue?.RemoveQueued(item);
+ }
}
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)
{
- Queue.ClearQueue();
- if (Queue.Current is not null)
+ Queue?.ClearQueue();
+ if (Queue?.Current is not null)
await Queue.Current.CancelAsync();
}
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;
}
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)
{
- string logText = string.Join("\r\n", _viewModel.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}"));
- await App.MainWindow.Clipboard.SetTextAsync(logText);
+ if (_viewModel is ProcessQueueViewModel vm)
+ {
+ 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)
{
- Queue.ClearQueue();
- if (Queue.Current is not null)
+ Queue?.ClearQueue();
+ if (Queue?.Current is not null)
await Queue.Current.CancelAsync();
}
private void btnClearFinished_Click(object sender, EventArgs e)
{
- Queue.ClearCompleted();
+ Queue?.ClearCompleted();
- if (!_viewModel.Running)
+ if (_viewModel?.Running is false)
_viewModel.RunningTime = string.Empty;
}
@@ -155,7 +165,7 @@ namespace LibationAvalonia.Views
{
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?)))
{
@@ -172,7 +182,7 @@ namespace LibationAvalonia.Views
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)
{
@@ -184,7 +194,7 @@ namespace LibationAvalonia.Views
: val.ToString("F2")
) + " MB/s";
}
- return value.ToString();
+ return value?.ToString();
}
}
}
diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml b/Source/LibationAvalonia/Views/ProductsDisplay.axaml
index fd90a1c1..90517420 100644
--- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml
+++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml
@@ -18,6 +18,7 @@
ItemsSource="{Binding GridEntries}"
CanUserSortColumns="True" BorderThickness="3"
CanUserResizeColumns="True"
+ LoadingRow="ProductsDisplay_LoadingRow"
CanUserReorderColumns="True">
@@ -93,7 +94,7 @@
-
+
@@ -103,7 +104,7 @@
-
+
@@ -113,7 +114,7 @@
-
+
@@ -123,7 +124,7 @@
-
+
@@ -133,7 +134,7 @@
-
+
@@ -143,7 +144,7 @@
-
+
@@ -153,7 +154,7 @@
-
+
@@ -163,7 +164,7 @@
-
+
@@ -177,14 +178,13 @@
MinWidth="10" Width="{Binding ProductRatingWidth, Mode=TwoWay}"
SortMemberPath="ProductRating" CanUserSort="True"
OpacityBinding="{CompiledBinding Liberate.Opacity}"
- BackgroundBinding="{CompiledBinding Liberate.BackgroundBrush}"
ClipboardContentBinding="{CompiledBinding ProductRating}"
Binding="{CompiledBinding ProductRating}" />
-
+
@@ -198,14 +198,13 @@
MinWidth="10" Width="{Binding MyRatingWidth, Mode=TwoWay}"
SortMemberPath="MyRating" CanUserSort="True"
OpacityBinding="{CompiledBinding Liberate.Opacity}"
- BackgroundBinding="{CompiledBinding Liberate.BackgroundBrush}"
ClipboardContentBinding="{CompiledBinding MyRating}"
Binding="{CompiledBinding MyRating, Mode=TwoWay}" />
-
+
@@ -215,7 +214,7 @@
-
+
diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
index 6f6948ab..7097aa56 100644
--- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
@@ -1,6 +1,8 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Input.Platform;
+using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using DataLayer;
@@ -17,16 +19,17 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+#nullable enable
namespace LibationAvalonia.Views
{
public partial class ProductsDisplay : UserControl
{
- public event EventHandler LiberateClicked;
- public event EventHandler LiberateSeriesClicked;
- public event EventHandler ConvertToMp3Clicked;
+ public event EventHandler? LiberateClicked;
+ public event EventHandler? LiberateSeriesClicked;
+ public event EventHandler? ConvertToMp3Clicked;
- private ProductsDisplayViewModel _viewModel => DataContext as ProductsDisplayViewModel;
- ImageDisplayDialog imageDisplayDialog;
+ private ProductsDisplayViewModel? _viewModel => DataContext as ProductsDisplayViewModel;
+ ImageDisplayDialog? imageDisplayDialog;
public ProductsDisplay()
{
@@ -52,6 +55,8 @@ namespace LibationAvalonia.Views
Configuration.Instance.PropertyChanged += Configuration_GridScaleChanged;
Configuration.Instance.PropertyChanged += Configuration_FontChanged;
+ #region Design Mode Testing
+#if DEBUG
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
@@ -80,6 +85,8 @@ namespace LibationAvalonia.Views
setFontScale(1);
return;
}
+#endif
+ #endregion
setGridScale(Configuration.Instance.GridScaleFactor);
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 entry && entry.Liberate.IsEpisode)
+ e.Row.DynamicResource(DataGridRow.BackgroundProperty, "SeriesEntryGridBackgroundBrush");
+ else
+ e.Row.Background = Brushes.Transparent;
+ }
+
private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is DataGridColumn col && e.Property == DataGridColumn.IsVisibleProperty)
@@ -105,13 +120,15 @@ namespace LibationAvalonia.Views
[PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
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))]
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;
@@ -171,17 +188,18 @@ namespace LibationAvalonia.Views
#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 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
{
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());
}
@@ -240,13 +258,14 @@ namespace LibationAvalonia.Views
{
try
{
- var window = this.GetParentWindow();
+ if (this.GetParentWindow() is not Window window)
+ return;
var openFileDialogOptions = new FilePickerOpenOptions
{
Title = ctx.LocateFileDialogTitle,
AllowMultiple = false,
- SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
+ SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix!),
FileTypeFilter = new FilePickerFileType[]
{
new("All files (*.*)") { Patterns = new[] { "*" } },
@@ -303,7 +322,7 @@ namespace LibationAvalonia.Views
{
var template = ctx.CreateTemplateEditor(libraryBook, existingTemplate);
var form = new EditTemplateDialog(template);
- if (await form.ShowDialog(this.GetParentWindow()) == DialogResult.OK)
+ if (this.GetParentWindow() is Window window && await form.ShowDialog(window) == DialogResult.OK)
{
setNewTemplate(template.EditingTemplate.TemplateText);
}
@@ -340,12 +359,12 @@ namespace LibationAvalonia.Views
#region View Bookmarks/Clips
- if (!entry.Liberate.IsSeries)
+ if (!entry.Liberate.IsSeries && VisualRoot is Window window)
{
args.ContextMenuItems.Add(new MenuItem
{
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);
+ if (HeaderCell_PI is null)
+ return;
+
foreach (var column in productsGrid.Columns)
{
var itemName = column.SortMemberPath;
@@ -406,8 +428,9 @@ namespace LibationAvalonia.Views
}
);
- var headercell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
- headercell.ContextMenu = contextMenu;
+ var headerCell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
+ if (headerCell is not null)
+ headerCell.ContextMenu = contextMenu;
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