diff --git a/Source/AppScaffolding/Ensure.cs b/Source/AppScaffolding/Ensure.cs
new file mode 100644
index 000000000..67c8c5ff4
--- /dev/null
+++ b/Source/AppScaffolding/Ensure.cs
@@ -0,0 +1,97 @@
+using System;
+
+namespace AppScaffolding;
+
+///
+/// A helper class for ensuring parameter conditions.
+/// Yoinked from https://github.com/CoryCharlton/CCSWE.Core/blob/master/src/Core/Ensure.cs
+///
+public static class Ensure
+{
+ private static Exception GetException(string name, string message) where TException : Exception, new()
+ {
+ Exception exception = (TException)Activator.CreateInstance(typeof(TException), message);
+
+ if (exception is ArgumentNullException)
+ {
+ return new ArgumentNullException(name, message);
+ }
+
+ if (exception is ArgumentOutOfRangeException)
+ {
+ return new ArgumentOutOfRangeException(name, message);
+ }
+
+ if (exception is ArgumentException)
+ {
+ return new ArgumentException(message, name);
+ }
+
+ return exception;
+ }
+
+ ///
+ /// Throws an if the expression evaluates to false .
+ ///
+ /// The name of the parameter we are validating.
+ /// The expression that will be evaluated.
+ /// The message associated with the
+ /// Thrown when the expression evaluates to false .
+ public static void IsInRange(string name, bool expression, string message = null)
+ {
+ IsValid(name, expression, string.IsNullOrWhiteSpace(message) ? $"The value passed for '{name}' is out of range." : message);
+ }
+
+ ///
+ /// Throws an if the value is null .
+ ///
+ /// The name of the parameter we are validating.
+ /// The value that will be evaluated.
+ /// The message associated with the
+ /// Thrown when the value is null .
+ public static void IsNotNull(string name, T value, string message = null)
+ {
+ IsValid(name, value != null, string.IsNullOrWhiteSpace(message) ? $"The value passed for '{name}' is null." : message);
+ }
+
+ ///
+ /// Throws an if the value is null or whitespace .
+ ///
+ /// The name of the parameter we are validating.
+ /// The value that will be evaluated.
+ /// The message associated with the
+ /// Thrown when the value is null or whitespace . .
+ public static void IsNotNullOrWhitespace(string name, string value, string message = null)
+ {
+ IsValid(name, !string.IsNullOrWhiteSpace(value), string.IsNullOrWhiteSpace(message) ? $"The value passed for '{name}' is empty, null, or whitespace." : message);
+ }
+
+ ///
+ /// Throws an if the expression evaluates to false .
+ ///
+ /// The name of the parameter we are validating.
+ /// The expression that will be evaluated.
+ /// The message associated with the
+ /// Thrown when the expression evaluates to false .
+ public static void IsValid(string name, bool expression, string message = null)
+ {
+ IsValid(name, expression, string.IsNullOrWhiteSpace(message) ? $"The value passed for '{name}' is not valid." : message);
+ }
+
+ ///
+ /// Throws an exception if the expression evaluates to false .
+ ///
+ /// The type of to throw.
+ /// The name of the parameter we are validating.
+ /// The expression that will be evaluated.
+ /// The message associated with the
+ public static void IsValid(string name, bool expression, string message = null) where TException : Exception, new()
+ {
+ if (expression)
+ {
+ return;
+ }
+
+ throw GetException(name, string.IsNullOrWhiteSpace(message) ? $"The value passed for '{name}' is not valid." : message);
+ }
+}
\ No newline at end of file
diff --git a/Source/AppScaffolding/SynchronizedObservableCollection.cs b/Source/AppScaffolding/SynchronizedObservableCollection.cs
new file mode 100644
index 000000000..7675fa29c
--- /dev/null
+++ b/Source/AppScaffolding/SynchronizedObservableCollection.cs
@@ -0,0 +1,774 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace AppScaffolding;
+
+/// Represents a thread-safe dynamic data collection
+/// that provides notifications when items get added, removed,
+/// or when the whole list is refreshed.
+/// Yoinked from: https://github.com/CoryCharlton/CCSWE.Core/blob/master/src/Core/Collections/ObjectModel/SynchronizedObservableCollection%601.cs
+/// The type of elements in the collection.
+#if NETSTANDARD2_0 || NETFULL
+ [Serializable]
+#endif
+[ComVisible(false)]
+[DebuggerDisplay("Count = {Count}")]
+public class SynchronizedObservableCollection : IDisposable, IList, IList, IReadOnlyList, INotifyCollectionChanged, INotifyPropertyChanged
+{
+ /// Initializes a new instance of the class.
+ public SynchronizedObservableCollection() : this(new List(), GetCurrentSynchronizationContext())
+ {
+ }
+
+ /// Initializes a new instance of the class that contains elements copied from the specified collection.
+ /// The collection from which the elements are copied.
+ /// The parameter cannot be null.
+ [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
+ public SynchronizedObservableCollection(IEnumerable collection) : this(collection, GetCurrentSynchronizationContext())
+ {
+ }
+
+ /// Initializes a new instance of the class with the specified context.
+ /// The context used for event invokation.
+ /// The parameter cannot be null.
+ public SynchronizedObservableCollection(SynchronizationContext context) : this(new List(), context)
+ {
+ }
+
+ /// Initializes a new instance of the class that contains elements copied from the specified collection with the specified context.
+ /// The collection from which the elements are copied.
+ /// The context used for event invokation.
+ /// The parameter cannot be null.
+ /// The parameter cannot be null.
+ [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
+ public SynchronizedObservableCollection(IEnumerable collection, SynchronizationContext context)
+ {
+ Ensure.IsNotNull(nameof(collection), collection);
+ Ensure.IsNotNull(nameof(context), context);
+
+ _context = context;
+
+ foreach (var item in collection)
+ {
+ _items.Add(item);
+ }
+ }
+
+#if NETSTANDARD2_0 || NETFULL
+ [NonSerialized]
+#endif
+ private readonly SynchronizationContext _context;
+ private bool _isDisposed;
+ private readonly IList _items = new List();
+#if NETSTANDARD2_0 || NETFULL
+ [NonSerialized]
+#endif
+ private readonly ReaderWriterLockSlim _itemsLocker = new ReaderWriterLockSlim();
+#if NETSTANDARD2_0 || NETFULL
+ [NonSerialized]
+#endif
+ private readonly SimpleMonitor _monitor = new SimpleMonitor();
+#if NETSTANDARD2_0 || NETFULL
+ [NonSerialized]
+#endif
+ private object _syncRoot;
+
+ event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
+ {
+ add { PropertyChanged += value; }
+ remove { PropertyChanged -= value; }
+ }
+
+ /// Occurs when an item is added, removed, changed, moved, or the entire list is refreshed.
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ /// Occurs when a property value changes.
+ protected event PropertyChangedEventHandler PropertyChanged;
+
+ /// Gets a value indicating whether the .
+ /// true if the has a fixed size; otherwise, false.
+ protected bool IsFixedSize => false;
+
+ bool IList.IsFixedSize => IsFixedSize;
+
+ /// Gets a value indicating whether the is read-only.
+ /// true if the is read-only; otherwise, false.
+ protected bool IsReadOnly => false;
+
+ bool ICollection.IsReadOnly => IsReadOnly;
+
+ bool IList.IsReadOnly => IsReadOnly;
+
+ /// Gets a value indicating whether access to the is synchronized (thread safe).
+ /// true if access to the is synchronized (thread safe); otherwise, false.
+ protected bool IsSynchronized => true;
+
+ bool ICollection.IsSynchronized => IsSynchronized;
+
+ /// Gets an object that can be used to synchronize access to the .
+ /// An object that can be used to synchronize access to the .
+ protected object SyncRoot
+ {
+ get
+ {
+ // ReSharper disable once InvertIf
+ if (_syncRoot == null)
+ {
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ var collection = _items as ICollection;
+ if (collection != null)
+ {
+ _syncRoot = collection.SyncRoot;
+ }
+ else
+ {
+ Interlocked.CompareExchange(ref _syncRoot, new object(), null);
+ }
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+
+ return _syncRoot;
+ }
+ }
+
+ object ICollection.SyncRoot => SyncRoot;
+
+ object IList.this[int index]
+ {
+ get { return this[index]; }
+ set
+ {
+ try
+ {
+ this[index] = (T)value;
+ }
+ catch (InvalidCastException)
+ {
+ throw new ArgumentException("'value' is the wrong type");
+ }
+ }
+ }
+
+ ///
+ /// Gets the that events will be invoked on.
+ ///
+ // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter
+ public SynchronizationContext Context => _context;
+
+ /// Gets the number of elements actually contained in the .
+ /// The number of elements actually contained in the .
+ public int Count
+ {
+ get
+ {
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ return _items.Count;
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+ }
+
+ /// Gets or sets the element at the specified index.
+ /// The element at the specified index.
+ /// The zero-based index of the element to get or set.
+ ///
+ /// is less than zero.-or- is equal to or greater than .
+ public T this[int index]
+ {
+ get
+ {
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ CheckIndex(index);
+
+ return _items[index];
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+ set
+ {
+ T oldValue;
+
+ _itemsLocker.EnterWriteLock();
+
+ try
+ {
+ CheckIndex(index);
+ CheckReentrancy();
+
+ oldValue = _items[index];
+
+ _items[index] = value;
+
+ }
+ finally
+ {
+ _itemsLocker.ExitWriteLock();
+ }
+
+ OnNotifyItemReplaced(value, oldValue, index);
+ }
+ }
+
+ private IDisposable BlockReentrancy()
+ {
+ _monitor.Enter();
+
+ return _monitor;
+ }
+
+ // ReSharper disable once UnusedParameter.Local
+ private void CheckIndex(int index)
+ {
+ if (index < 0 || index >= _items.Count)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private void CheckReentrancy()
+ {
+ if (_monitor.Busy && CollectionChanged != null && CollectionChanged.GetInvocationList().Length > 1)
+ {
+ throw new InvalidOperationException("SynchronizedObservableCollection reentrancy not allowed");
+ }
+ }
+
+ private static SynchronizationContext GetCurrentSynchronizationContext()
+ {
+ return SynchronizationContext.Current ?? new SynchronizationContext();
+ }
+
+ private static bool IsCompatibleObject(object value)
+ {
+ // Non-null values are fine. Only accept nulls if T is a class or Nullable.
+ // Note that default(T) is not equal to null for value types except when T is Nullable.
+ return ((value is T) || (value == null && default(T) == null));
+ }
+
+ private void OnNotifyCollectionReset()
+ {
+ using (BlockReentrancy())
+ {
+ _context.Send(state =>
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }, null);
+ }
+ }
+
+ private void OnNotifyItemAdded(T item, int index)
+ {
+ using (BlockReentrancy())
+ {
+ _context.Send(state =>
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
+ }, null);
+ }
+ }
+
+ private void OnNotifyItemMoved(T item, int newIndex, int oldIndex)
+ {
+ using (BlockReentrancy())
+ {
+ _context.Send(state =>
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex));
+ }, null);
+ }
+ }
+
+ private void OnNotifyItemRemoved(T item, int index)
+ {
+ using (BlockReentrancy())
+ {
+ _context.Send(state =>
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
+ }, null);
+ }
+ }
+
+ private void OnNotifyItemReplaced(T newItem, T oldItem, int index)
+ {
+ using (BlockReentrancy())
+ {
+ _context.Send(state =>
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, index));
+ }, null);
+ }
+ }
+
+ ///
+ /// Releases all resources used by the .
+ ///
+ /// Not used.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _itemsLocker.Dispose();
+ _isDisposed = true;
+ }
+
+ /// Adds an object to the end of the .
+ /// The object to be added to the end of the . The value can be null for reference types.
+ public void Add(T item)
+ {
+ _itemsLocker.EnterWriteLock();
+
+ int index;
+
+ try
+ {
+ CheckReentrancy();
+
+ index = _items.Count;
+
+ _items.Insert(index, item);
+ }
+ finally
+ {
+ _itemsLocker.ExitWriteLock();
+ }
+
+ OnNotifyItemAdded(item, index);
+ }
+
+ int IList.Add(object value)
+ {
+ _itemsLocker.EnterWriteLock();
+
+ int index;
+ T item;
+
+ try
+ {
+ CheckReentrancy();
+
+ index = _items.Count;
+ item = (T)value;
+
+ _items.Insert(index, item);
+ }
+ catch (InvalidCastException)
+ {
+ throw new ArgumentException("'value' is the wrong type");
+ }
+ finally
+ {
+ _itemsLocker.ExitWriteLock();
+ }
+
+ OnNotifyItemAdded(item, index);
+
+ return index;
+ }
+
+ /// Removes all elements from the .
+ public void Clear()
+ {
+ _itemsLocker.EnterWriteLock();
+
+ try
+ {
+ CheckReentrancy();
+
+ _items.Clear();
+ }
+ finally
+ {
+ _itemsLocker.ExitWriteLock();
+ }
+
+ OnNotifyCollectionReset();
+ }
+
+ /// Copies the elements to an existing one-dimensional , starting at the specified array index.
+ /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing.
+ /// The zero-based index in at which copying begins.
+ ///
+ /// is null.
+ ///
+ /// is less than zero.
+ /// The number of elements in the source is greater than the available space from to the end of the destination .
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ Ensure.IsNotNull(nameof(array), array);
+ Ensure.IsInRange(nameof(arrayIndex), arrayIndex >= 0 && arrayIndex < array.Length);
+ Ensure.IsValid(nameof(arrayIndex), array.Length - arrayIndex >= Count, "Invalid offset length.");
+
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ _items.CopyTo(array, arrayIndex);
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+
+ void ICollection.CopyTo(Array array, int arrayIndex)
+ {
+ Ensure.IsNotNull(nameof(array), array);
+ Ensure.IsValid(nameof(array), array.Rank == 1, "Multidimensional array are not supported");
+ Ensure.IsValid(nameof(array), array.GetLowerBound(0) == 0, "Non-zero lower bound is not supported");
+ Ensure.IsInRange(nameof(arrayIndex), arrayIndex >= 0 && arrayIndex < array.Length);
+ Ensure.IsValid(nameof(arrayIndex), array.Length - arrayIndex >= Count, "Invalid offset length.");
+
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ var tArray = array as T[];
+ if (tArray != null)
+ {
+ _items.CopyTo(tArray, arrayIndex);
+ }
+ else
+ {
+
+#if NETSTANDARD2_0 || NETFULL
+ //
+ // Catch the obvious case assignment will fail.
+ // We can found all possible problems by doing the check though.
+ // For example, if the element type of the Array is derived from T,
+ // we can't figure out if we can successfully copy the element beforehand.
+ //
+ var targetType = array.GetType().GetElementType();
+ var sourceType = typeof (T);
+ if (!(targetType.IsAssignableFrom(sourceType) || sourceType.IsAssignableFrom(targetType)))
+ {
+ throw new ArrayTypeMismatchException("Invalid array type");
+ }
+#endif
+
+ //
+ // We can't cast array of value type to object[], so we don't support
+ // widening of primitive types here.
+ //
+ var objects = array as object[];
+ if (objects == null)
+ {
+ throw new ArrayTypeMismatchException("Invalid array type");
+ }
+
+ var count = _items.Count;
+ try
+ {
+ for (var i = 0; i < count; i++)
+ {
+ objects[arrayIndex++] = _items[i];
+ }
+ }
+ catch (ArrayTypeMismatchException)
+ {
+ throw new ArrayTypeMismatchException("Invalid array type");
+ }
+ }
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+
+ /// Determines whether an element is in the .
+ /// true if is found in the ; otherwise, false.
+ /// The object to locate in the . The value can be null for reference types.
+ public bool Contains(T item)
+ {
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ return _items.Contains(item);
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+
+ bool IList.Contains(object value)
+ {
+ if (!IsCompatibleObject(value))
+ {
+ return false;
+ }
+
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ return _items.Contains((T)value);
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+
+ ///
+ /// Releases all resources used by the .
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// Returns an enumerator that iterates through the .
+ /// An for the .
+ public IEnumerator GetEnumerator()
+ {
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ return _items.ToList().GetEnumerator();
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ // ReSharper disable once RedundantCast
+ return (IEnumerator)GetEnumerator();
+ }
+
+ /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire .
+ /// The zero-based index of the first occurrence of within the entire , if found; otherwise, -1.
+ /// The object to locate in the . The value can be null for reference types.
+ public int IndexOf(T item)
+ {
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ return _items.IndexOf(item);
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+
+ int IList.IndexOf(object value)
+ {
+ if (!IsCompatibleObject(value))
+ {
+ return -1;
+ }
+
+ _itemsLocker.EnterReadLock();
+
+ try
+ {
+ return _items.IndexOf((T)value);
+ }
+ finally
+ {
+ _itemsLocker.ExitReadLock();
+ }
+ }
+
+ /// Inserts an element into the at the specified index.
+ /// The zero-based index at which should be inserted.
+ /// The object to insert. The value can be null for reference types.
+ ///
+ /// is less than zero.-or- is greater than .
+ public void Insert(int index, T item)
+ {
+ _itemsLocker.EnterWriteLock();
+
+ try
+ {
+ CheckReentrancy();
+
+ if (index < 0 || index > _items.Count)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ _items.Insert(index, item);
+ }
+ finally
+ {
+ _itemsLocker.ExitWriteLock();
+ }
+
+ OnNotifyItemAdded(item, index);
+ }
+
+ void IList.Insert(int index, object value)
+ {
+ try
+ {
+ Insert(index, (T)value);
+ }
+ catch (InvalidCastException)
+ {
+ throw new ArgumentException("'value' is the wrong type");
+ }
+ }
+
+ /// Moves the item at the specified index to a new location in the collection.
+ /// The zero-based index specifying the location of the item to be moved.
+ /// The zero-based index specifying the new location of the item.
+ public void Move(int oldIndex, int newIndex)
+ {
+ T item;
+
+ _itemsLocker.EnterWriteLock();
+
+ try
+ {
+ CheckReentrancy();
+ CheckIndex(oldIndex);
+ CheckIndex(newIndex);
+
+ item = _items[oldIndex];
+
+ _items.RemoveAt(oldIndex);
+ _items.Insert(newIndex, item);
+ }
+ finally
+ {
+ _itemsLocker.ExitWriteLock();
+ }
+
+ OnNotifyItemMoved(item, newIndex, oldIndex);
+ }
+
+ /// Removes the first occurrence of a specific object from the .
+ /// true if is successfully removed; otherwise, false. This method also returns false if was not found in the original .
+ /// The object to remove from the . The value can be null for reference types.
+ public bool Remove(T item)
+ {
+ int index;
+ T value;
+
+ _itemsLocker.EnterWriteLock();
+
+ try
+ {
+ CheckReentrancy();
+
+ index = _items.IndexOf(item);
+
+ if (index < 0)
+ {
+ return false;
+ }
+
+ value = _items[index];
+
+ _items.RemoveAt(index);
+ }
+ finally
+ {
+ _itemsLocker.ExitWriteLock();
+ }
+
+ OnNotifyItemRemoved(value, index);
+
+ return true;
+ }
+
+ void IList.Remove(object value)
+ {
+ if (IsCompatibleObject(value))
+ {
+ Remove((T)value);
+ }
+ }
+
+ /// Removes the element at the specified index of the .
+ /// The zero-based index of the element to remove.
+ ///
+ /// is less than zero.-or- is equal to or greater than .
+ public void RemoveAt(int index)
+ {
+ T value;
+
+ _itemsLocker.EnterWriteLock();
+
+ try
+ {
+ CheckIndex(index);
+ CheckReentrancy();
+
+ value = _items[index];
+
+ _items.RemoveAt(index);
+ }
+ finally
+ {
+ _itemsLocker.ExitWriteLock();
+ }
+
+ OnNotifyItemRemoved(value, index);
+ }
+
+ private class SimpleMonitor : IDisposable
+ {
+ private int _busyCount;
+
+ public bool Busy => _busyCount > 0;
+
+ public void Enter()
+ {
+ ++_busyCount;
+ }
+
+ public void Dispose()
+ {
+ --_busyCount;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/FileManager/BackgroundFileSystem.cs b/Source/FileManager/BackgroundFileSystem.cs
index e4ec7db20..fc3e10337 100644
--- a/Source/FileManager/BackgroundFileSystem.cs
+++ b/Source/FileManager/BackgroundFileSystem.cs
@@ -72,8 +72,8 @@ private void Init()
fileSystemWatcher.Renamed += FileSystemWatcher_Changed;
fileSystemWatcher.Error += FileSystemWatcher_Error;
- backgroundScanner = new Task(BackgroundScanner);
- backgroundScanner.Start();
+ backgroundScanner = Task.Factory.StartNew(BackgroundScanner, TaskCreationOptions.LongRunning);
+ //backgroundScanner.Start();
}
private void Stop()
{
diff --git a/Source/Libation.sln.startup.json b/Source/Libation.sln.startup.json
new file mode 100644
index 000000000..e4def5e0a
--- /dev/null
+++ b/Source/Libation.sln.startup.json
@@ -0,0 +1,24 @@
+/*
+ This is a configuration file for the SwitchStartupProject Visual Studio Extension
+ See https://github.com/ernstc/SwitchStartupProject2022/blob/main/Configuration.md
+*/
+{
+ /* Configuration File Version */
+ "Version": 3,
+
+ /* Create an item in the dropdown list for each project in the solution? */
+ "ListAllProjects": false,
+
+ "MultiProjectConfigurations": {
+ "WinForms": {
+ "Projects": {
+ "LibationWinForms": {}
+ }
+ },
+ "Avalonia": {
+ "Projects": {
+ "LibationAvalonia": {}
+ }
+ }
+ }
+}
diff --git a/Source/LibationAvalonia/App.axaml b/Source/LibationAvalonia/App.axaml
index 5ba253c07..444237d30 100644
--- a/Source/LibationAvalonia/App.axaml
+++ b/Source/LibationAvalonia/App.axaml
@@ -1,92 +1,113 @@
-
+
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs
index 59fe3f75b..7d65996ee 100644
--- a/Source/LibationAvalonia/App.axaml.cs
+++ b/Source/LibationAvalonia/App.axaml.cs
@@ -2,232 +2,256 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling;
using LibationAvalonia.Dialogs;
+using LibationAvalonia.ViewModels;
using LibationAvalonia.Views;
using LibationFileManager;
+using LibationUiBase;
+using LibationUiBase.ViewModels;
+using LibationUiBase.ViewModels.Player;
using System;
using System.Collections.Generic;
using System.IO;
+using System.Reflection;
using System.Threading.Tasks;
-using ReactiveUI;
-using DataLayer;
+using ViewModelBase = LibationAvalonia.ViewModels.ViewModelBase;
namespace LibationAvalonia
{
- public class App : Application
- {
- public static Window 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 readonly Uri AssetUriBase = new("avares://Libation/Assets/");
- public static Stream OpenAsset(string assetRelativePath)
- => AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
-
- public override void Initialize()
- {
- AvaloniaXamlLoader.Load(this);
- }
-
- public static Task> LibraryTask;
-
- public override void OnFrameworkInitializationCompleted()
- {
- if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
- {
- var config = Configuration.Instance;
-
- if (!config.LibationSettingsAreValid)
- {
- var defaultLibationFilesDir = Configuration.UserProfile;
-
- // check for existing settings in default location
- var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
- if (Configuration.SettingsFileIsValid(defaultSettingsFile))
- Configuration.SetLibationFiles(defaultLibationFilesDir);
-
- if (config.LibationSettingsAreValid)
- {
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- ShowMainWindow(desktop);
- }
- else
- {
- var setupDialog = new SetupDialog { Config = config };
- setupDialog.Closing += Setup_Closing;
- desktop.MainWindow = setupDialog;
- }
- }
- else
- ShowMainWindow(desktop);
- }
-
- base.OnFrameworkInitializationCompleted();
- }
-
- private async void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
- {
- var setupDialog = sender as SetupDialog;
- var desktop = ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
-
- try
- {
- // all returns should be preceded by either:
- // - if config.LibationSettingsAreValid
- // - error message, Exit()
- if (setupDialog.IsNewUser)
- {
- Configuration.SetLibationFiles(Configuration.UserProfile);
- setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books));
-
- if (setupDialog.Config.LibationSettingsAreValid)
- {
+ public class App : Application
+ {
+ public static Window 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 readonly Uri AssetUriBase = new("avares://Libation/Assets/");
+ public static Stream OpenAsset(string assetRelativePath)
+ => AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
+
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public static Task> LibraryTask;
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ RegisterTypes();
+
+ ServiceLocator.AddCommonServicesAndBuild();
+
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ var config = Configuration.Instance;
+
+ if (!config.LibationSettingsAreValid)
+ {
+ var defaultLibationFilesDir = Configuration.UserProfile;
+
+ // check for existing settings in default location
+ var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
+ if (Configuration.SettingsFileIsValid(defaultSettingsFile))
+ Configuration.SetLibationFiles(defaultLibationFilesDir);
+
+ if (config.LibationSettingsAreValid)
+ {
+ LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
+ ShowMainWindow(desktop);
+ }
+ else
+ {
+ var setupDialog = new SetupDialog { Config = config };
+ setupDialog.Closing += Setup_Closing;
+ desktop.MainWindow = setupDialog;
+ }
+ }
+ else
+ ShowMainWindow(desktop);
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ private async void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+ {
+ var setupDialog = sender as SetupDialog;
+ var desktop = ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
+
+ try
+ {
+ // all returns should be preceded by either:
+ // - if config.LibationSettingsAreValid
+ // - error message, Exit()
+ if (setupDialog.IsNewUser)
+ {
+ Configuration.SetLibationFiles(Configuration.UserProfile);
+ setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books));
+
+ if (setupDialog.Config.LibationSettingsAreValid)
+ {
string theme = setupDialog.SelectedTheme.Content as string;
-
- setupDialog.Config.SetString(theme, nameof(ThemeVariant));
-
- await RunMigrationsAsync(setupDialog.Config);
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
- ShowMainWindow(desktop);
- }
- else
- await CancelInstallation();
- }
- else if (setupDialog.IsReturningUser)
- {
- ShowLibationFilesDialog(desktop, setupDialog.Config, OnLibationFilesCompleted);
- }
- else
- {
- await CancelInstallation();
- return;
- }
-
- }
- catch (Exception ex)
- {
- var title = "Fatal error, pre-logging";
- var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
- try
- {
- await MessageBox.ShowAdminAlert(null, body, title, ex);
- }
- catch
- {
- await MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
- return;
- }
- }
-
- private async Task RunMigrationsAsync(Configuration config)
- {
- // most migrations go in here
- AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
-
- await MessageBox.VerboseLoggingWarning_ShowIfTrue();
-
- // logging is init'd here
- AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config);
- }
-
- private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action OnClose)
- {
- var libationFilesDialog = new LibationFilesDialog();
- desktop.MainWindow = libationFilesDialog;
- libationFilesDialog.Show();
-
- void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
- {
- libationFilesDialog.Closing -= WindowClosing;
- e.Cancel = true;
- OnClose?.Invoke(desktop, libationFilesDialog, config);
- }
- libationFilesDialog.Closing += WindowClosing;
- }
-
- private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
- {
- Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
- if (config.LibationSettingsAreValid)
- {
- await RunMigrationsAsync(config);
-
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
- ShowMainWindow(desktop);
- }
- else
- {
- // path did not result in valid settings
- var continueResult = await MessageBox.Show(
- $"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
- "New install?",
- MessageBoxButtons.YesNo,
- MessageBoxIcon.Question);
-
- if (continueResult == DialogResult.Yes)
- {
- config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
-
- if (config.LibationSettingsAreValid)
- {
- await RunMigrationsAsync(config);
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
- ShowMainWindow(desktop);
- }
- else
- await CancelInstallation();
- }
- else
- await CancelInstallation();
- }
-
- libationFilesDialog.Close();
- }
-
- static async Task CancelInstallation()
- {
- await MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
- Environment.Exit(0);
- }
-
- private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
- {
+
+ setupDialog.Config.SetString(theme, nameof(ThemeVariant));
+
+ await RunMigrationsAsync(setupDialog.Config);
+ LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
+ AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
+ ShowMainWindow(desktop);
+ }
+ else
+ await CancelInstallation();
+ }
+ else if (setupDialog.IsReturningUser)
+ {
+ ShowLibationFilesDialog(desktop, setupDialog.Config, OnLibationFilesCompleted);
+ }
+ else
+ {
+ await CancelInstallation();
+ return;
+ }
+
+ }
+ catch (Exception ex)
+ {
+ var title = "Fatal error, pre-logging";
+ var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
+ try
+ {
+ await MessageBox.ShowAdminAlert(null, body, title, ex);
+ }
+ catch
+ {
+ await MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ return;
+ }
+ }
+
+ private async Task RunMigrationsAsync(Configuration config)
+ {
+ // most migrations go in here
+ AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
+
+ await MessageBox.VerboseLoggingWarning_ShowIfTrue();
+
+ // logging is init'd here
+ AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config);
+ }
+
+ private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action OnClose)
+ {
+ var libationFilesDialog = new LibationFilesDialog();
+ desktop.MainWindow = libationFilesDialog;
+ libationFilesDialog.Show();
+
+ void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
+ {
+ libationFilesDialog.Closing -= WindowClosing;
+ e.Cancel = true;
+ OnClose?.Invoke(desktop, libationFilesDialog, config);
+ }
+ libationFilesDialog.Closing += WindowClosing;
+ }
+
+ private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
+ {
+ Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
+ if (config.LibationSettingsAreValid)
+ {
+ await RunMigrationsAsync(config);
+
+ LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
+ AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
+ ShowMainWindow(desktop);
+ }
+ else
+ {
+ // path did not result in valid settings
+ var continueResult = await MessageBox.Show(
+ $"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
+ "New install?",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question);
+
+ if (continueResult == DialogResult.Yes)
+ {
+ config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
+
+ if (config.LibationSettingsAreValid)
+ {
+ await RunMigrationsAsync(config);
+ LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
+ AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
+ ShowMainWindow(desktop);
+ }
+ else
+ await CancelInstallation();
+ }
+ else
+ await CancelInstallation();
+ }
+
+ libationFilesDialog.Close();
+ }
+
+ static async Task CancelInstallation()
+ {
+ await MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ Environment.Exit(0);
+ }
+
+ 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"
+ // "System"
_ => ThemeVariant.Default
};
//Reload colors for current theme
- LoadStyles();
- var mainWindow = new MainWindow();
- desktop.MainWindow = MainWindow = mainWindow;
- mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
- mainWindow.RestoreSizeAndLocation(Configuration.Instance);
- mainWindow.Show();
- }
-
- private static void LoadStyles()
- {
- 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));
- }
- }
+ LoadStyles();
+ var mainWindow = new MainWindow();
+ desktop.MainWindow = MainWindow = mainWindow;
+ mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
+ mainWindow.RestoreSizeAndLocation(Configuration.Instance);
+ mainWindow.Show();
+ }
+
+ private static void LoadStyles()
+ {
+ 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));
+ }
+
+ private static void RegisterTypes()
+ {
+ ServiceLocator.RegisterTransient();
+ ServiceLocator.RegisterSingleton(typeof(SidebarViewModel));
+ ServiceLocator.RegisterSingleton(typeof(PlayerViewModel));
+ ServiceLocator.RegisterSingleton(typeof(ProcessQueueViewModel));
+
+ // Register VMs here only.
+ foreach (var type in Assembly.GetExecutingAssembly().GetExportedTypes())
+ {
+ if (type.IsSubclassOf(typeof(ViewModelBase)) && !type.IsAbstract)
+ ServiceLocator.RegisterTransient(type);
+ }
+
+ // Add more types as needed here.
+ }
+ }
}
diff --git a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml
index 984e39d6f..9a6e373b4 100644
--- a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml
@@ -35,7 +35,7 @@
MinHeight="25"
Height="25"
VerticalAlignment="Center"
- SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
+ SelectedItem="{Binding SelectedBook, Mode=TwoWay}"
ItemsSource="{Binding BookStatuses}">
diff --git a/Source/LibationAvalonia/LibationAvalonia.csproj b/Source/LibationAvalonia/LibationAvalonia.csproj
index 91c8de6fc..02e96762e 100644
--- a/Source/LibationAvalonia/LibationAvalonia.csproj
+++ b/Source/LibationAvalonia/LibationAvalonia.csproj
@@ -57,6 +57,9 @@
LiberateStatusButton.axaml
+
+ SidebarControl.axaml
+
MainVM.cs
@@ -79,6 +82,7 @@
+
@@ -87,6 +91,11 @@
+
+
+
+
+
diff --git a/Source/LibationAvalonia/ViewLocator.cs b/Source/LibationAvalonia/ViewLocator.cs
index c604e0830..3459cb45c 100644
--- a/Source/LibationAvalonia/ViewLocator.cs
+++ b/Source/LibationAvalonia/ViewLocator.cs
@@ -18,7 +18,7 @@ public Control Build(object data)
}
else
{
- return new TextBlock { Text = "Not Found: " + name };
+ return new TextBlock { Text = $"View Not Found: {name}" };
}
}
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs
index f54870222..3d05a7c43 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs
@@ -19,7 +19,7 @@ public void BackupAllBooks()
Serilog.Log.Logger.Information("Begin backing up all library books");
- ProcessQueue.AddDownloadDecrypt(
+ Sidebar.ProcessQueue.AddDownloadDecrypt(
DbContexts
.GetLibrary_Flat_NoTracking()
.UnLiberated()
@@ -34,7 +34,7 @@ public void BackupAllBooks()
public void BackupAllPdfs()
{
setQueueCollapseState(false);
- ProcessQueue.AddDownloadPdf(DbContexts.GetLibrary_Flat_NoTracking().Where(lb => lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated));
+ Sidebar.ProcessQueue.AddDownloadPdf(DbContexts.GetLibrary_Flat_NoTracking().Where(lb => lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated));
}
public async Task ConvertAllToMp3Async()
@@ -50,7 +50,7 @@ public async Task ConvertAllToMp3Async()
if (result == DialogResult.Yes)
{
setQueueCollapseState(false);
- ProcessQueue.AddConvertMp3(DbContexts.GetLibrary_Flat_NoTracking().Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is ContentType.Product));
+ Sidebar.ProcessQueue.AddConvertMp3(DbContexts.GetLibrary_Flat_NoTracking().Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is ContentType.Product));
}
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
}
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs
index 9c30a7cc0..f29daaf57 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs
@@ -40,13 +40,13 @@ public async void LiberateClicked(LibraryBook libraryBook)
{
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", libraryBook);
setQueueCollapseState(false);
- ProcessQueue.AddDownloadDecrypt(libraryBook);
+ Sidebar.ProcessQueue.AddDownloadDecrypt(libraryBook);
}
else if (libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
setQueueCollapseState(false);
- ProcessQueue.AddDownloadPdf(libraryBook);
+ Sidebar.ProcessQueue.AddDownloadPdf(libraryBook);
}
else if (libraryBook.Book.Audio_Exists())
{
@@ -56,7 +56,7 @@ public async void LiberateClicked(LibraryBook libraryBook)
if (!Go.To.File(filePath?.ShortPathName))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
- await MessageBox.Show($"File not found" + suffix);
+ await MessageBox.Show($"File not found: {suffix}");
}
}
}
@@ -74,7 +74,7 @@ public void LiberateSeriesClicked(ISeriesEntry series)
Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook);
- ProcessQueue.AddDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated());
+ Sidebar.ProcessQueue.AddDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated());
}
catch (Exception ex)
{
@@ -90,7 +90,7 @@ public void ConvertToMp3Clicked(LibraryBook libraryBook)
{
Serilog.Log.Logger.Information("Begin convert to mp3 {libraryBook}", libraryBook);
setQueueCollapseState(false);
- ProcessQueue.AddConvertMp3(libraryBook);
+ Sidebar.ProcessQueue.AddConvertMp3(libraryBook);
}
}
catch (Exception ex)
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs b/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs
index 2dd4829ca..8706ff698 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs
@@ -6,78 +6,76 @@
using System.Collections.Generic;
using System.Linq;
-namespace LibationAvalonia.ViewModels
-{
- partial class MainVM
- {
- private readonly InterruptableTimer autoScanTimer = new InterruptableTimer(TimeSpan.FromMinutes(5));
+namespace LibationAvalonia.ViewModels;
- private void Configure_ScanAuto()
- {
- // subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
- autoScanTimer.Elapsed += async (_, __) =>
- {
- using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
- var accounts = persister.AccountsSettings
- .GetAll()
- .Where(a => a.LibraryScan)
- .ToArray();
+partial class MainVM
+{
+ private readonly InterruptableTimer autoScanTimer = new InterruptableTimer(TimeSpan.FromMinutes(5));
- // in autoScan, new books SHALL NOT show dialog
- try
- {
- await LibraryCommands.ImportAccountAsync(LibationAvalonia.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts);
- }
- catch (OperationCanceledException)
- {
- Serilog.Log.Information("Audible login attempt cancelled by user");
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, "Error invoking auto-scan");
- }
- };
+ private void Configure_ScanAuto()
+ {
+ // subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
+ autoScanTimer.Elapsed += async (_, __) =>
+ {
+ using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
+ var accounts = persister.AccountsSettings
+ .GetAll()
+ .Where(a => a.LibraryScan)
+ .ToArray();
- // if enabled: begin on load
- MainWindow.Loaded += startAutoScan;
+ // in autoScan, new books SHALL NOT show dialog
+ try
+ {
+ await LibraryCommands.ImportAccountAsync(LibationAvalonia.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts);
+ }
+ catch (OperationCanceledException)
+ {
+ Serilog.Log.Information("Audible login attempt cancelled by user");
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Logger.Error(ex, "Error invoking auto-scan");
+ }
+ };
- // if new 'default' account is added, run autoscan
- AccountsSettingsPersister.Saving += accountsPreSave;
- AccountsSettingsPersister.Saved += accountsPostSave;
+ // if enabled: begin on load
+ MainWindow.Loaded += startAutoScan;
- // when autoscan setting is changed, update menu checkbox and run autoscan
- Configuration.Instance.PropertyChanged += startAutoScan;
- }
+ // if new 'default' account is added, run autoscan
+ AccountsSettingsPersister.Saving += accountsPreSave;
+ AccountsSettingsPersister.Saved += accountsPostSave;
+ // when autoscan setting is changed, update menu checkbox and run autoscan
+ Configuration.Instance.PropertyChanged += startAutoScan;
+ }
- private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
- private List<(string AccountId, string LocaleName)> getDefaultAccounts()
- {
- using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
- return persister.AccountsSettings
- .GetAll()
- .Where(a => a.LibraryScan)
- .Select(a => (a.AccountId, a.Locale.Name))
- .ToList();
- }
+ private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
+ private List<(string AccountId, string LocaleName)> getDefaultAccounts()
+ {
+ using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
+ return persister.AccountsSettings
+ .GetAll()
+ .Where(a => a.LibraryScan)
+ .Select(a => (a.AccountId, a.Locale.Name))
+ .ToList();
+ }
- private void accountsPreSave(object sender = null, EventArgs e = null)
- => preSaveDefaultAccounts = getDefaultAccounts();
+ private void accountsPreSave(object sender = null, EventArgs e = null)
+ => preSaveDefaultAccounts = getDefaultAccounts();
- private void accountsPostSave(object sender = null, EventArgs e = null)
- {
- if (getDefaultAccounts().Except(preSaveDefaultAccounts).Any())
- startAutoScan();
- }
+ private void accountsPostSave(object sender = null, EventArgs e = null)
+ {
+ if (getDefaultAccounts().Except(preSaveDefaultAccounts).Any())
+ startAutoScan();
+ }
- [PropertyChangeFilter(nameof(Configuration.AutoScan))]
- private void startAutoScan(object sender = null, EventArgs e = null)
- {
- AutoScanChecked = Configuration.Instance.AutoScan;
- if (AutoScanChecked)
- autoScanTimer.PerformNow();
- else
- autoScanTimer.Stop();
- }
- }
-}
+ [PropertyChangeFilter(nameof(Configuration.AutoScan))]
+ private void startAutoScan(object sender = null, EventArgs e = null)
+ {
+ AutoScanChecked = Configuration.Instance.AutoScan;
+ if (AutoScanChecked)
+ autoScanTimer.PerformNow();
+ else
+ autoScanTimer.Stop();
+ }
+}
\ No newline at end of file
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs
index db6fe17bc..166c22cdd 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs
@@ -74,7 +74,7 @@ public void LiberateVisible()
Serilog.Log.Logger.Information("Begin backing up visible library books");
- ProcessQueue.AddDownloadDecrypt(
+ Sidebar.ProcessQueue.AddDownloadDecrypt(
ProductsDisplay
.GetVisibleBookEntries()
.UnLiberated()
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.cs b/Source/LibationAvalonia/ViewModels/MainVM.cs
index 86c69d9d9..69b367c82 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.cs
@@ -1,25 +1,27 @@
using ApplicationServices;
using LibationAvalonia.Views;
using LibationFileManager;
+using LibationUiBase;
using ReactiveUI;
+using Splat;
namespace LibationAvalonia.ViewModels
{
- public partial class MainVM : ViewModelBase
+ public partial class MainVM : ViewModelBase
{
- public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();
- public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
+ public SidebarViewModel Sidebar { get; } = ServiceLocator.Get();
+ public ProductsDisplayViewModel ProductsDisplay { get; } = ServiceLocator.Get();
private double? _downloadProgress = null;
public double? DownloadProgress { get => _downloadProgress; set => this.RaiseAndSetIfChanged(ref _downloadProgress, value); }
+ public readonly MainWindow MainWindow;
- private readonly MainWindow MainWindow;
- public MainVM(MainWindow mainWindow)
- {
- MainWindow = mainWindow;
+ public MainVM(MainWindow mainWindow)
+ {
+ MainWindow = mainWindow;
- ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation";
+ ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation";
LibraryCommands.LibrarySizeChanged += async (_, _) => await ProductsDisplay.UpdateGridAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
Configure_NonUI();
diff --git a/Source/LibationAvalonia/ViewModels/SidebarViewModel.cs b/Source/LibationAvalonia/ViewModels/SidebarViewModel.cs
new file mode 100644
index 000000000..7a86c2330
--- /dev/null
+++ b/Source/LibationAvalonia/ViewModels/SidebarViewModel.cs
@@ -0,0 +1,11 @@
+using LibationUiBase;
+using LibationUiBase.ViewModels.Player;
+
+namespace LibationAvalonia.ViewModels
+{
+ public class SidebarViewModel
+ {
+ public PlayerViewModel Player { get; } = ServiceLocator.Get();
+ public ProcessQueueViewModel ProcessQueue { get; } = ServiceLocator.Get();
+ }
+}
diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml b/Source/LibationAvalonia/Views/MainWindow.axaml
index 49f015992..cc2eea74a 100644
--- a/Source/LibationAvalonia/Views/MainWindow.axaml
+++ b/Source/LibationAvalonia/Views/MainWindow.axaml
@@ -1,242 +1,417 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs
index 87823b173..2112f8e28 100644
--- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs
+++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using LibationUiBase;
namespace LibationAvalonia.Views
{
@@ -16,7 +17,9 @@ public partial class MainWindow : ReactiveWindow
public event EventHandler> LibraryLoaded;
public MainWindow()
{
- DataContext = new MainVM(this);
+ var vm = new MainVM(this);
+ DataContext = vm;
+
InitializeComponent();
Configure_Upgrade();
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
deleted file mode 100644
index 03323d4e6..000000000
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
+++ /dev/null
@@ -1,151 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Process Queue
-
-
-
-
-
-
-
-
- Cancel All
-
-
-
-
-
-
-
- Clear Finished
-
-
-
-
-
-
- Queue Log
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Copy Log Entries to Clipboard
- Clear Log
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
deleted file mode 100644
index 0b7761d84..000000000
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
+++ /dev/null
@@ -1,190 +0,0 @@
-using ApplicationServices;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Data.Converters;
-using DataLayer;
-using LibationAvalonia.ViewModels;
-using LibationUiBase;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-
-namespace LibationAvalonia.Views
-{
- public partial class ProcessQueueControl : UserControl
- {
- private TrackedQueue Queue => _viewModel.Queue;
- private ProcessQueueViewModel _viewModel => DataContext as ProcessQueueViewModel;
-
- public ProcessQueueControl()
- {
- InitializeComponent();
-
- ProcessBookControl.PositionButtonClicked += ProcessBookControl2_ButtonClicked;
- ProcessBookControl.CancelButtonClicked += ProcessBookControl2_CancelButtonClicked;
-
- #region Design Mode Testing
- if (Design.IsDesignMode)
- {
- var vm = new ProcessQueueViewModel();
- var Logger = LogMe.RegisterForm(vm);
- DataContext = vm;
- using var context = DbContexts.GetContext();
- List testList = new()
- {
- new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
- {
- Result = ProcessBookResult.FailedAbort,
- Status = ProcessBookStatus.Failed,
- },
- new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), Logger)
- {
- Result = ProcessBookResult.FailedSkip,
- Status = ProcessBookStatus.Failed,
- },
- new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), Logger)
- {
- Result = ProcessBookResult.FailedRetry,
- Status = ProcessBookStatus.Failed,
- },
- new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), Logger)
- {
- Result = ProcessBookResult.ValidationFail,
- Status = ProcessBookStatus.Failed,
- },
- new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), Logger)
- {
- Result = ProcessBookResult.Cancelled,
- Status = ProcessBookStatus.Cancelled,
- },
- new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), Logger)
- {
- Result = ProcessBookResult.Success,
- Status = ProcessBookStatus.Completed,
- },
- new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), Logger)
- {
- Result = ProcessBookResult.None,
- Status = ProcessBookStatus.Working,
- },
- new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
- {
- Result = ProcessBookResult.None,
- Status = ProcessBookStatus.Queued,
- },
- };
-
- vm.Queue.Enqueue(testList);
- vm.Queue.MoveNext();
- vm.Queue.MoveNext();
- vm.Queue.MoveNext();
- vm.Queue.MoveNext();
- vm.Queue.MoveNext();
- vm.Queue.MoveNext();
- vm.Queue.MoveNext();
- return;
- }
- #endregion
- }
-
- public void NumericUpDown_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
- {
- if (e.Key == Avalonia.Input.Key.Enter && sender is Avalonia.Input.IInputElement input) input.Focus();
- }
-
- #region Control event handlers
-
- private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item)
- {
- if (item is not null)
- await item.CancelAsync();
- Queue.RemoveQueued(item);
- }
-
- private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel item, QueuePosition queueButton)
- {
- Queue.MoveQueuePosition(item, queueButton);
- }
-
- public async void CancelAllBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- Queue.ClearQueue();
- if (Queue.Current is not null)
- await Queue.Current.CancelAsync();
- }
-
- public void ClearFinishedBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- Queue.ClearCompleted();
-
- if (!_viewModel.Running)
- _viewModel.RunningTime = string.Empty;
- }
-
- public void ClearLogBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- _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);
- }
-
- private async void cancelAllBtn_Click(object sender, EventArgs e)
- {
- Queue.ClearQueue();
- if (Queue.Current is not null)
- await Queue.Current.CancelAsync();
- }
-
- private void btnClearFinished_Click(object sender, EventArgs e)
- {
- Queue.ClearCompleted();
-
- if (!_viewModel.Running)
- _viewModel.RunningTime = string.Empty;
- }
-
- #endregion
- }
-
- public class DecimalConverter : IValueConverter
- {
- public static readonly DecimalConverter Instance = new();
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is string sourceText && targetType.IsAssignableTo(typeof(decimal?)))
- {
- if (sourceText == "∞") return 0;
-
- for (int i = sourceText.Length; i > 0; i--)
- {
- if (decimal.TryParse(sourceText[..i], out var val))
- return val;
- }
-
- return 0;
- }
- return 0;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is decimal val)
- {
- return
- val == 0 ? "∞"
- : (
- val >= 10 ? ((long)val).ToString()
- : val >= 1 ? val.ToString("F1")
- : val.ToString("F2")
- ) + " MB/s";
- }
- return value.ToString();
- }
- }
-}
diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml b/Source/LibationAvalonia/Views/ProductsDisplay.axaml
index fd90a1c19..68f191fbe 100644
--- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml
+++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml
@@ -1,260 +1,387 @@
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
index 6f6948ab4..d2d0bc3bd 100644
--- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
@@ -16,580 +16,599 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using AppScaffolding;
+using LibationUiBase;
+using System.Diagnostics;
+using Dinah.Core.Logging;
+using LibationUiBase.ViewModels.Player;
namespace LibationAvalonia.Views
{
- public partial class ProductsDisplay : UserControl
- {
- public event EventHandler LiberateClicked;
- public event EventHandler LiberateSeriesClicked;
- public event EventHandler ConvertToMp3Clicked;
-
- private ProductsDisplayViewModel _viewModel => DataContext as ProductsDisplayViewModel;
- ImageDisplayDialog imageDisplayDialog;
-
- public ProductsDisplay()
- {
- InitializeComponent();
- DataGridContextMenus.CellContextMenuStripNeeded += ProductsGrid_CellContextMenuStripNeeded;
-
- var cellSelector = Selectors.Is(null);
- rowHeightStyle = new Style(_ => cellSelector);
- rowHeightStyle.Setters.Add(rowHeightSetter);
-
- var tboxSelector = cellSelector.Descendant().Is();
- fontSizeStyle = new Style(_ => tboxSelector);
- fontSizeStyle.Setters.Add(fontSizeSetter);
-
- var tboxH1Selector = cellSelector.Child().Is().Child().Is().Class("h1");
- fontSizeH1Style = new Style(_ => tboxH1Selector);
- fontSizeH1Style.Setters.Add(fontSizeH1Setter);
-
- var tboxH2Selector = cellSelector.Child().Is().Child().Is().Class("h2");
- fontSizeH2Style = new Style(_ => tboxH2Selector);
- fontSizeH2Style.Setters.Add(fontSizeH2Setter);
-
- Configuration.Instance.PropertyChanged += Configuration_GridScaleChanged;
- Configuration.Instance.PropertyChanged += Configuration_FontChanged;
-
- if (Design.IsDesignMode)
- {
- using var context = DbContexts.GetContext();
- List sampleEntries;
- try
- {
- sampleEntries = new()
- {
+ public partial class ProductsDisplay : UserControl
+ {
+ public event EventHandler LiberateClicked;
+ public event EventHandler LiberateSeriesClicked;
+ public event EventHandler ConvertToMp3Clicked;
+
+ private PlayerViewModel _playerViewModel = ServiceLocator.Get();
+ private ProductsDisplayViewModel _viewModel => DataContext as ProductsDisplayViewModel;
+ ImageDisplayDialog imageDisplayDialog;
+
+ public ProductsDisplay()
+ {
+ InitializeComponent();
+ DataGridContextMenus.CellContextMenuStripNeeded += ProductsGrid_CellContextMenuStripNeeded;
+
+ var cellSelector = Selectors.Is(null);
+ rowHeightStyle = new Style(_ => cellSelector);
+ rowHeightStyle.Setters.Add(rowHeightSetter);
+
+ var tboxSelector = cellSelector.Descendant().Is();
+ fontSizeStyle = new Style(_ => tboxSelector);
+ fontSizeStyle.Setters.Add(fontSizeSetter);
+
+ var tboxH1Selector = cellSelector.Child().Is().Child().Is().Class("h1");
+ fontSizeH1Style = new Style(_ => tboxH1Selector);
+ fontSizeH1Style.Setters.Add(fontSizeH1Setter);
+
+ var tboxH2Selector = cellSelector.Child().Is().Child().Is().Class("h2");
+ fontSizeH2Style = new Style(_ => tboxH2Selector);
+ fontSizeH2Style.Setters.Add(fontSizeH2Setter);
+
+ Configuration.Instance.PropertyChanged += Configuration_GridScaleChanged;
+ Configuration.Instance.PropertyChanged += Configuration_FontChanged;
+
+ if (Design.IsDesignMode)
+ {
+ using var context = DbContexts.GetContext();
+ List sampleEntries;
+ try
+ {
+ sampleEntries = new()
+ {
//context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU"),try{
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"),
- context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
- context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
- context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
- context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
- context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
- context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")
- };
- }
- catch { sampleEntries = new(); }
-
- var pdvm = new ProductsDisplayViewModel();
- _ = pdvm.BindToGridAsync(sampleEntries);
- DataContext = pdvm;
-
- setGridScale(1);
- setFontScale(1);
- return;
- }
-
- setGridScale(Configuration.Instance.GridScaleFactor);
- setFontScale(Configuration.Instance.GridFontScaleFactor);
- Configure_ColumnCustomization();
-
- foreach (var column in productsGrid.Columns)
- {
- column.CustomSortComparer = new RowComparer(column);
- }
- }
-
- private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
- {
- if (sender is DataGridColumn col && e.Property == DataGridColumn.IsVisibleProperty)
- {
- col.DisplayIndex = 0;
- col.CanUserReorder = false;
- }
- }
-
- #region Scaling
-
- [PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
- private void Configuration_GridScaleChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
- {
- setGridScale((float)e.NewValue);
- }
-
- [PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
- private void Configuration_FontChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
- {
- setFontScale((float)e.NewValue);
- }
-
- private readonly Style rowHeightStyle;
- private readonly Setter rowHeightSetter = new() { Property = DataGridCell.HeightProperty };
-
- private readonly Style fontSizeStyle;
- private readonly Setter fontSizeSetter = new() { Property = TextBlock.FontSizeProperty };
-
- private readonly Style fontSizeH1Style;
- private readonly Setter fontSizeH1Setter = new() { Property = TextBlock.FontSizeProperty };
-
- private readonly Style fontSizeH2Style;
- private readonly Setter fontSizeH2Setter = new() { Property = TextBlock.FontSizeProperty };
-
- private void setFontScale(double scaleFactor)
- {
- const double TextBlockFontSize = 11;
- const double H1FontSize = 14;
- const double H2FontSize = 12;
-
- fontSizeSetter.Value = TextBlockFontSize * scaleFactor;
- fontSizeH1Setter.Value = H1FontSize * scaleFactor;
- fontSizeH2Setter.Value = H2FontSize * scaleFactor;
-
- productsGrid.Styles.Remove(fontSizeStyle);
- productsGrid.Styles.Remove(fontSizeH1Style);
- productsGrid.Styles.Remove(fontSizeH2Style);
- productsGrid.Styles.Add(fontSizeStyle);
- productsGrid.Styles.Add(fontSizeH1Style);
- productsGrid.Styles.Add(fontSizeH2Style);
- }
-
- private void setGridScale(double scaleFactor)
- {
- const float BaseRowHeight = 80;
- const float BaseLiberateWidth = 75;
- const float BaseCoverWidth = 80;
-
- foreach (var column in productsGrid.Columns)
- {
- switch (column.SortMemberPath)
- {
- case nameof(IGridEntry.Liberate):
- column.Width = new DataGridLength(BaseLiberateWidth * scaleFactor);
- break;
- case nameof(IGridEntry.Cover):
- column.Width = new DataGridLength(BaseCoverWidth * scaleFactor);
- break;
- }
- }
- rowHeightSetter.Value = BaseRowHeight * scaleFactor;
- productsGrid.Styles.Remove(rowHeightStyle);
- productsGrid.Styles.Add(rowHeightStyle);
- }
-
- #endregion
-
- #region Cell Context Menu
-
- 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")
- {
- args.ContextMenuItems.Add(new MenuItem
- {
- Header = ctx.CopyCellText,
- Command = ReactiveCommand.CreateFromTask(() => App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents))
- });
- args.ContextMenuItems.Add(new Separator());
- }
-
- #region Liberate all Episodes
-
- if (entry.Liberate.IsSeries)
- {
- args.ContextMenuItems.Add(new MenuItem()
- {
- Header = ctx.LiberateEpisodesText,
- IsEnabled = ctx.LiberateEpisodesEnabled,
- Command = ReactiveCommand.Create(() => LiberateSeriesClicked?.Invoke(this, (ISeriesEntry)entry))
- });
- }
-
- #endregion
- #region Set Download status to Downloaded
-
- args.ContextMenuItems.Add(new MenuItem()
- {
- Header = ctx.SetDownloadedText,
- IsEnabled = ctx.SetDownloadedEnabled,
- Command = ReactiveCommand.Create(ctx.SetDownloaded)
- });
-
- #endregion
- #region Set Download status to Not Downloaded
-
- args.ContextMenuItems.Add(new MenuItem()
- {
- Header = ctx.SetNotDownloadedText,
- IsEnabled = ctx.SetNotDownloadedEnabled,
- Command = ReactiveCommand.Create(ctx.SetNotDownloaded)
- });
-
- #endregion
- #region Remove from library
-
- args.ContextMenuItems.Add(new MenuItem
- {
- Header = ctx.RemoveText,
- Command = ReactiveCommand.CreateFromTask(ctx.RemoveAsync)
- });
-
- #endregion
-
- if (!entry.Liberate.IsSeries)
- {
- #region Locate file
-
- args.ContextMenuItems.Add(new MenuItem
- {
- Header = ctx.LocateFileText,
- Command = ReactiveCommand.CreateFromTask(async () =>
- {
- try
- {
- var window = this.GetParentWindow();
-
- var openFileDialogOptions = new FilePickerOpenOptions
- {
- Title = ctx.LocateFileDialogTitle,
- AllowMultiple = false,
- SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
- FileTypeFilter = new FilePickerFileType[]
- {
- new("All files (*.*)") { Patterns = new[] { "*" } },
- }
- };
-
- var selectedFiles = await window.StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
- var selectedFile = selectedFiles.SingleOrDefault()?.TryGetLocalPath();
-
- if (selectedFile is not null)
- FilePathCache.Insert(entry.AudibleProductId, selectedFile);
- }
- catch (Exception ex)
- {
- await MessageBox.ShowAdminAlert(null, ctx.LocateFileErrorMessage, ctx.LocateFileErrorMessage, ex);
- }
- })
- });
-
- #endregion
- #region Convert to Mp3
- args.ContextMenuItems.Add(new MenuItem
- {
- Header = ctx.ConvertToMp3Text,
- IsEnabled = ctx.ConvertToMp3Enabled,
- Command = ReactiveCommand.Create(() => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook))
- });
-
- #endregion
- }
-
- #region Force Re-Download
- if (!entry.Liberate.IsSeries)
- {
- args.ContextMenuItems.Add(new MenuItem()
- {
- Header = ctx.ReDownloadText,
- IsEnabled = ctx.ReDownloadEnabled,
- Command = ReactiveCommand.Create(() =>
- {
- //No need to persist this change. It only needs to last long for the file to start downloading
- entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
- LiberateClicked?.Invoke(this, entry.LibraryBook);
- })
- });
- }
- #endregion
-
- args.ContextMenuItems.Add(new Separator());
-
- #region Edit Templates
- async Task editTemplate(LibraryBook libraryBook, string existingTemplate, Action setNewTemplate)
- where T : Templates, LibationFileManager.ITemplate, new()
- {
- var template = ctx.CreateTemplateEditor(libraryBook, existingTemplate);
- var form = new EditTemplateDialog(template);
- if (await form.ShowDialog(this.GetParentWindow()) == DialogResult.OK)
- {
- setNewTemplate(template.EditingTemplate.TemplateText);
- }
- }
-
- if (!entry.Liberate.IsSeries)
- {
- args.ContextMenuItems.Add(new MenuItem
- {
- Header = ctx.EditTemplatesText,
- ItemsSource = new[]
- {
- new MenuItem
- {
- Header = ctx.FolderTemplateText,
- Command = ReactiveCommand.CreateFromTask(() => editTemplate(entry.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t))
- },
- new MenuItem
- {
- Header = ctx.FileTemplateText,
- Command = ReactiveCommand.CreateFromTask(() => editTemplate(entry.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t))
- },
- new MenuItem
- {
- Header = ctx.MultipartTemplateText,
- Command = ReactiveCommand.CreateFromTask(() => editTemplate(entry.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t))
- }
- }
- });
- args.ContextMenuItems.Add(new Separator());
- }
-
- #endregion
-
- #region View Bookmarks/Clips
-
- if (!entry.Liberate.IsSeries)
- {
- args.ContextMenuItems.Add(new MenuItem
- {
- Header = ctx.ViewBookmarksText,
- Command = ReactiveCommand.CreateFromTask(() => new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window))
- });
- }
-
- #endregion
- #region View All Series
-
- if (entry.Book.SeriesLink.Any())
- {
- args.ContextMenuItems.Add(new MenuItem
- {
- Header = ctx.ViewSeriesText,
- Command = ReactiveCommand.Create(() => new SeriesViewDialog(entry.LibraryBook).Show())
- });
- }
-
- #endregion
- }
-
- #endregion
-
- #region Column Customizations
-
- private void Configure_ColumnCustomization()
- {
- if (Design.IsDesignMode) return;
-
- productsGrid.ColumnDisplayIndexChanged += ProductsGrid_ColumnDisplayIndexChanged;
-
- var config = Configuration.Instance;
- var gridColumnsVisibilities = config.GridColumnsVisibilities;
- var displayIndices = config.GridColumnsDisplayIndices;
-
- var contextMenu = new ContextMenu();
- contextMenu.Closed += ContextMenu_MenuClosed;
- contextMenu.Opening += ContextMenu_ContextMenuOpening;
- List menuItems = new();
- contextMenu.ItemsSource = menuItems;
-
- menuItems.Add(new MenuItem { Header = "Show / Hide Columns" });
- menuItems.Add(new MenuItem { Header = "-" });
-
- var HeaderCell_PI = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-
- foreach (var column in productsGrid.Columns)
- {
- var itemName = column.SortMemberPath;
-
- if (itemName == nameof(IGridEntry.Remove))
- continue;
-
- menuItems.Add
- (
- new MenuItem
- {
- Header = ((string)column.Header).Replace('\n', ' '),
- Tag = column,
- Icon = new CheckBox(),
- }
- );
-
- var headercell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
- headercell.ContextMenu = contextMenu;
-
- column.IsVisible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
- }
-
- //We must set DisplayIndex properties in ascending order
- foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
- {
- if (!productsGrid.Columns.Any(c => c.SortMemberPath == itemName))
- continue;
-
- var column = productsGrid.Columns
- .Single(c => c.SortMemberPath == itemName);
-
- column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, productsGrid.Columns.IndexOf(column));
- }
- }
-
- private void ContextMenu_ContextMenuOpening(object sender, System.ComponentModel.CancelEventArgs e)
- {
- var contextMenu = sender as ContextMenu;
- foreach (var mi in contextMenu.Items.OfType())
- {
- if (mi.Tag is DataGridColumn column)
- {
- var cbox = mi.Icon as CheckBox;
- cbox.IsChecked = column.IsVisible;
- }
- }
- }
-
- private void ContextMenu_MenuClosed(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- var contextMenu = sender as ContextMenu;
- var config = Configuration.Instance;
- var dictionary = config.GridColumnsVisibilities;
-
- foreach (var mi in contextMenu.Items.OfType())
- {
- if (mi.Tag is DataGridColumn column)
- {
- var cbox = mi.Icon as CheckBox;
- column.IsVisible = cbox.IsChecked == true;
- dictionary[column.SortMemberPath] = cbox.IsChecked == true;
- }
- }
-
- //If all columns are hidden, register the context menu on the grid so users can unhide.
- if (!productsGrid.Columns.Any(c => c.IsVisible))
- productsGrid.ContextMenu = contextMenu;
- else
- productsGrid.ContextMenu = null;
-
- config.GridColumnsVisibilities = dictionary;
- }
-
- private void ProductsGrid_ColumnDisplayIndexChanged(object sender, DataGridColumnEventArgs e)
- {
- var config = Configuration.Instance;
-
- var dictionary = config.GridColumnsDisplayIndices;
- dictionary[e.Column.SortMemberPath] = e.Column.DisplayIndex;
- config.GridColumnsDisplayIndices = dictionary;
- }
-
- #endregion
-
- #region Button Click Handlers
-
- public async void LiberateButton_Click(object sender, EventArgs e)
- {
- var button = sender as LiberateStatusButton;
-
- if (button.DataContext is ISeriesEntry sEntry)
- {
- await _viewModel.ToggleSeriesExpanded(sEntry);
-
- //Expanding and collapsing reset the list, which will cause focus to shift
- //to the topright cell. Reset focus onto the clicked button's cell.
- button.Focus();
- }
- else if (button.DataContext is ILibraryBookEntry lbEntry)
- {
- LiberateClicked?.Invoke(this, lbEntry.LibraryBook);
- }
- }
-
- public void CloseImageDisplay()
- {
- if (imageDisplayDialog is not null && imageDisplayDialog.IsVisible)
- imageDisplayDialog.Close();
- }
-
- public void Version_DoubleClick(object sender, Avalonia.Input.TappedEventArgs args)
- {
- if (sender is Control panel && panel.DataContext is ILibraryBookEntry lbe && lbe.LastDownload.IsValid)
- lbe.LastDownload.OpenReleaseUrl();
- }
-
- public void Cover_Click(object sender, Avalonia.Input.TappedEventArgs args)
- {
- if (sender is not Image tblock || tblock.DataContext is not IGridEntry gEntry)
- return;
-
- if (imageDisplayDialog is null || !imageDisplayDialog.IsVisible)
- {
- imageDisplayDialog = new ImageDisplayDialog();
- }
-
- var picDef = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
-
- void PictureCached(object sender, PictureCachedEventArgs e)
- {
- if (e.Definition.PictureId == picDef.PictureId)
- imageDisplayDialog.SetCoverBytes(e.Picture);
-
- PictureStorage.PictureCached -= PictureCached;
- }
-
- PictureStorage.PictureCached += PictureCached;
- (bool isDefault, byte[] initialImageBts) = PictureStorage.GetPicture(picDef);
-
-
- var windowTitle = $"{gEntry.Title} - Cover";
-
-
- imageDisplayDialog.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(gEntry.LibraryBook);
- imageDisplayDialog.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(gEntry.LibraryBook, ".jpg"));
- imageDisplayDialog.Title = windowTitle;
- imageDisplayDialog.SetCoverBytes(initialImageBts);
-
- if (!isDefault)
- PictureStorage.PictureCached -= PictureCached;
-
- if (imageDisplayDialog.IsVisible)
- imageDisplayDialog.Activate();
- else
- imageDisplayDialog.Show();
- }
-
- public void Description_Click(object sender, Avalonia.Input.TappedEventArgs args)
- {
- if (sender is Control tblock && tblock.DataContext is IGridEntry gEntry)
- {
- var pt = tblock.PointToScreen(tblock.Bounds.TopRight);
- var displayWindow = new DescriptionDisplayDialog
- {
- SpawnLocation = new Point(pt.X, pt.Y),
- DescriptionText = gEntry.Description,
- };
-
- void CloseWindow(object o, DataGridRowEventArgs e)
- {
- displayWindow.Close();
- }
- productsGrid.LoadingRow += CloseWindow;
- displayWindow.Closing += (_, _) =>
- {
- productsGrid.LoadingRow -= CloseWindow;
- };
-
- displayWindow.Show();
- }
- }
-
- BookDetailsDialog bookDetailsForm;
-
- public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
- {
- var button = args.Source as Button;
-
- if (button.DataContext is ILibraryBookEntry lbEntry && VisualRoot is Window window)
- {
- if (bookDetailsForm is null || !bookDetailsForm.IsVisible)
- {
- bookDetailsForm = new BookDetailsDialog(lbEntry.LibraryBook);
- bookDetailsForm.Show(window);
- }
- else
- bookDetailsForm.LibraryBook = lbEntry.LibraryBook;
- }
- }
-
- #endregion
- }
+ context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
+ context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
+ context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
+ context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
+ context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
+ context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")
+ };
+ }
+ catch { sampleEntries = new(); }
+
+ var pdvm = new ProductsDisplayViewModel();
+ _ = pdvm.BindToGridAsync(sampleEntries);
+ DataContext = pdvm;
+
+ setGridScale(1);
+ setFontScale(1);
+ return;
+ }
+
+ setGridScale(Configuration.Instance.GridScaleFactor);
+ setFontScale(Configuration.Instance.GridFontScaleFactor);
+ Configure_ColumnCustomization();
+
+ foreach (var column in productsGrid.Columns)
+ {
+ column.CustomSortComparer = new RowComparer(column);
+ }
+ }
+
+ private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (sender is DataGridColumn col && e.Property == DataGridColumn.IsVisibleProperty)
+ {
+ col.DisplayIndex = 0;
+ col.CanUserReorder = false;
+ }
+ }
+
+ #region Scaling
+
+ [PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
+ private void Configuration_GridScaleChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
+ {
+ setGridScale((float)e.NewValue);
+ }
+
+ [PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
+ private void Configuration_FontChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
+ {
+ setFontScale((float)e.NewValue);
+ }
+
+ private readonly Style rowHeightStyle;
+ private readonly Setter rowHeightSetter = new() { Property = DataGridCell.HeightProperty };
+
+ private readonly Style fontSizeStyle;
+ private readonly Setter fontSizeSetter = new() { Property = TextBlock.FontSizeProperty };
+
+ private readonly Style fontSizeH1Style;
+ private readonly Setter fontSizeH1Setter = new() { Property = TextBlock.FontSizeProperty };
+
+ private readonly Style fontSizeH2Style;
+ private readonly Setter fontSizeH2Setter = new() { Property = TextBlock.FontSizeProperty };
+
+ private void setFontScale(double scaleFactor)
+ {
+ const double TextBlockFontSize = 11;
+ const double H1FontSize = 14;
+ const double H2FontSize = 12;
+
+ fontSizeSetter.Value = TextBlockFontSize * scaleFactor;
+ fontSizeH1Setter.Value = H1FontSize * scaleFactor;
+ fontSizeH2Setter.Value = H2FontSize * scaleFactor;
+
+ productsGrid.Styles.Remove(fontSizeStyle);
+ productsGrid.Styles.Remove(fontSizeH1Style);
+ productsGrid.Styles.Remove(fontSizeH2Style);
+ productsGrid.Styles.Add(fontSizeStyle);
+ productsGrid.Styles.Add(fontSizeH1Style);
+ productsGrid.Styles.Add(fontSizeH2Style);
+ }
+
+ private void setGridScale(double scaleFactor)
+ {
+ const float BaseRowHeight = 80;
+ const float BaseLiberateWidth = 75;
+ const float BaseCoverWidth = 80;
+
+ foreach (var column in productsGrid.Columns)
+ {
+ switch (column.SortMemberPath)
+ {
+ case nameof(IGridEntry.Liberate):
+ column.Width = new DataGridLength(BaseLiberateWidth * scaleFactor);
+ break;
+ case nameof(IGridEntry.Cover):
+ column.Width = new DataGridLength(BaseCoverWidth * scaleFactor);
+ break;
+ }
+ }
+ rowHeightSetter.Value = BaseRowHeight * scaleFactor;
+ productsGrid.Styles.Remove(rowHeightStyle);
+ productsGrid.Styles.Add(rowHeightStyle);
+ }
+
+ #endregion
+
+ #region Cell Context Menu
+
+ 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")
+ {
+ args.ContextMenuItems.Add(new MenuItem
+ {
+ Header = ctx.CopyCellText,
+ Command = ReactiveCommand.CreateFromTask(() => App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents))
+ });
+ args.ContextMenuItems.Add(new Separator());
+ }
+
+ #region Liberate all Episodes
+
+ if (entry.Liberate.IsSeries)
+ {
+ args.ContextMenuItems.Add(new MenuItem()
+ {
+ Header = ctx.LiberateEpisodesText,
+ IsEnabled = ctx.LiberateEpisodesEnabled,
+ Command = ReactiveCommand.Create(() => LiberateSeriesClicked?.Invoke(this, (ISeriesEntry)entry))
+ });
+ }
+
+ #endregion
+ #region Set Download status to Downloaded
+
+ args.ContextMenuItems.Add(new MenuItem()
+ {
+ Header = ctx.SetDownloadedText,
+ IsEnabled = ctx.SetDownloadedEnabled,
+ Command = ReactiveCommand.Create(ctx.SetDownloaded)
+ });
+
+ #endregion
+ #region Set Download status to Not Downloaded
+
+ args.ContextMenuItems.Add(new MenuItem()
+ {
+ Header = ctx.SetNotDownloadedText,
+ IsEnabled = ctx.SetNotDownloadedEnabled,
+ Command = ReactiveCommand.Create(ctx.SetNotDownloaded)
+ });
+
+ #endregion
+ #region Remove from library
+
+ args.ContextMenuItems.Add(new MenuItem
+ {
+ Header = ctx.RemoveText,
+ Command = ReactiveCommand.CreateFromTask(ctx.RemoveAsync)
+ });
+
+ #endregion
+
+ if (!entry.Liberate.IsSeries)
+ {
+ #region Locate file
+
+ args.ContextMenuItems.Add(new MenuItem
+ {
+ Header = ctx.LocateFileText,
+ Command = ReactiveCommand.CreateFromTask(async () =>
+ {
+ try
+ {
+ var window = this.GetParentWindow();
+
+ var openFileDialogOptions = new FilePickerOpenOptions
+ {
+ Title = ctx.LocateFileDialogTitle,
+ AllowMultiple = false,
+ SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
+ FileTypeFilter = new FilePickerFileType[]
+ {
+ new("All files (*.*)") { Patterns = new[] { "*" } },
+ }
+ };
+
+ var selectedFiles = await window.StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
+ var selectedFile = selectedFiles.SingleOrDefault()?.TryGetLocalPath();
+
+ if (selectedFile is not null)
+ FilePathCache.Insert(entry.AudibleProductId, selectedFile);
+ }
+ catch (Exception ex)
+ {
+ await MessageBox.ShowAdminAlert(null, ctx.LocateFileErrorMessage, ctx.LocateFileErrorMessage, ex);
+ }
+ })
+ });
+
+ #endregion
+ #region Convert to Mp3
+ args.ContextMenuItems.Add(new MenuItem
+ {
+ Header = ctx.ConvertToMp3Text,
+ IsEnabled = ctx.ConvertToMp3Enabled,
+ Command = ReactiveCommand.Create(() => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook))
+ });
+
+ #endregion
+ }
+
+ #region Force Re-Download
+ if (!entry.Liberate.IsSeries)
+ {
+ args.ContextMenuItems.Add(new MenuItem()
+ {
+ Header = ctx.ReDownloadText,
+ IsEnabled = ctx.ReDownloadEnabled,
+ Command = ReactiveCommand.Create(() =>
+ {
+ //No need to persist this change. It only needs to last long for the file to start downloading
+ entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
+ LiberateClicked?.Invoke(this, entry.LibraryBook);
+ })
+ });
+ }
+ #endregion
+
+ args.ContextMenuItems.Add(new Separator());
+
+ #region Edit Templates
+ async Task editTemplate(LibraryBook libraryBook, string existingTemplate, Action setNewTemplate)
+ where T : Templates, LibationFileManager.ITemplate, new()
+ {
+ var template = ctx.CreateTemplateEditor(libraryBook, existingTemplate);
+ var form = new EditTemplateDialog(template);
+ if (await form.ShowDialog(this.GetParentWindow()) == DialogResult.OK)
+ {
+ setNewTemplate(template.EditingTemplate.TemplateText);
+ }
+ }
+
+ if (!entry.Liberate.IsSeries)
+ {
+ args.ContextMenuItems.Add(new MenuItem
+ {
+ Header = ctx.EditTemplatesText,
+ ItemsSource = new[]
+ {
+ new MenuItem
+ {
+ Header = ctx.FolderTemplateText,
+ Command = ReactiveCommand.CreateFromTask(() => editTemplate(entry.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t))
+ },
+ new MenuItem
+ {
+ Header = ctx.FileTemplateText,
+ Command = ReactiveCommand.CreateFromTask(() => editTemplate(entry.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t))
+ },
+ new MenuItem
+ {
+ Header = ctx.MultipartTemplateText,
+ Command = ReactiveCommand.CreateFromTask(() => editTemplate(entry.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t))
+ }
+ }
+ });
+ args.ContextMenuItems.Add(new Separator());
+ }
+
+ #endregion
+
+ #region View Bookmarks/Clips
+
+ if (!entry.Liberate.IsSeries)
+ {
+ args.ContextMenuItems.Add(new MenuItem
+ {
+ Header = ctx.ViewBookmarksText,
+ Command = ReactiveCommand.CreateFromTask(() => new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window))
+ });
+ }
+
+ #endregion
+ #region View All Series
+
+ if (entry.Book.SeriesLink.Any())
+ {
+ args.ContextMenuItems.Add(new MenuItem
+ {
+ Header = ctx.ViewSeriesText,
+ Command = ReactiveCommand.Create(() => new SeriesViewDialog(entry.LibraryBook).Show())
+ });
+ }
+
+ #endregion
+ }
+
+ #endregion
+
+ #region Column Customizations
+
+ private void Configure_ColumnCustomization()
+ {
+ if (Design.IsDesignMode) return;
+
+ productsGrid.ColumnDisplayIndexChanged += ProductsGrid_ColumnDisplayIndexChanged;
+
+ var config = Configuration.Instance;
+ var gridColumnsVisibilities = config.GridColumnsVisibilities;
+ var displayIndices = config.GridColumnsDisplayIndices;
+
+ var contextMenu = new ContextMenu();
+ contextMenu.Closed += ContextMenu_MenuClosed;
+ contextMenu.Opening += ContextMenu_ContextMenuOpening;
+ List menuItems = new();
+ contextMenu.ItemsSource = menuItems;
+
+ menuItems.Add(new MenuItem { Header = "Show / Hide Columns" });
+ menuItems.Add(new MenuItem { Header = "-" });
+
+ var HeaderCell_PI = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ foreach (var column in productsGrid.Columns)
+ {
+ var itemName = column.SortMemberPath;
+
+ if (itemName == nameof(IGridEntry.Remove) || itemName == null)
+ continue;
+
+ menuItems.Add
+ (
+ new MenuItem
+ {
+ Header = ((string)column.Header)?.Replace('\n', ' '),
+ Tag = column,
+ Icon = new CheckBox(),
+ }
+ );
+
+ var headercell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
+ headercell.ContextMenu = contextMenu;
+
+ column.IsVisible = itemName != null ? gridColumnsVisibilities.GetValueOrDefault(itemName, true) : true;
+ }
+
+ //We must set DisplayIndex properties in ascending order
+ foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
+ {
+ if (!productsGrid.Columns.Any(c => c.SortMemberPath == itemName))
+ continue;
+
+ var column = productsGrid.Columns
+ .Single(c => c.SortMemberPath == itemName);
+
+ column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, productsGrid.Columns.IndexOf(column));
+ }
+ }
+
+ private void ContextMenu_ContextMenuOpening(object sender, System.ComponentModel.CancelEventArgs e)
+ {
+ var contextMenu = sender as ContextMenu;
+ foreach (var mi in contextMenu.Items.OfType())
+ {
+ if (mi.Tag is DataGridColumn column)
+ {
+ var cbox = mi.Icon as CheckBox;
+ cbox.IsChecked = column.IsVisible;
+ }
+ }
+ }
+
+ private void ContextMenu_MenuClosed(object sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ var contextMenu = sender as ContextMenu;
+ var config = Configuration.Instance;
+ var dictionary = config.GridColumnsVisibilities;
+
+ foreach (var mi in contextMenu.Items.OfType())
+ {
+ if (mi.Tag is DataGridColumn column)
+ {
+ var cbox = mi.Icon as CheckBox;
+ column.IsVisible = cbox.IsChecked == true;
+ dictionary[column.SortMemberPath] = cbox.IsChecked == true;
+ }
+ }
+
+ //If all columns are hidden, register the context menu on the grid so users can unhide.
+ if (!productsGrid.Columns.Any(c => c.IsVisible))
+ productsGrid.ContextMenu = contextMenu;
+ else
+ productsGrid.ContextMenu = null;
+
+ config.GridColumnsVisibilities = dictionary;
+ }
+
+ private void ProductsGrid_ColumnDisplayIndexChanged(object sender, DataGridColumnEventArgs e)
+ {
+ var config = Configuration.Instance;
+
+ var dictionary = config.GridColumnsDisplayIndices;
+ dictionary[e.Column.SortMemberPath] = e.Column.DisplayIndex;
+ config.GridColumnsDisplayIndices = dictionary;
+ }
+
+ #endregion
+
+ #region Button Click Handlers
+
+ public async void LiberateButton_Click(object sender, EventArgs e)
+ {
+ var button = sender as LiberateStatusButton;
+
+ if (button.DataContext is ISeriesEntry sEntry)
+ {
+ await _viewModel.ToggleSeriesExpanded(sEntry);
+
+ //Expanding and collapsing reset the list, which will cause focus to shift
+ //to the topright cell. Reset focus onto the clicked button's cell.
+ button.Focus();
+ }
+ else if (button.DataContext is ILibraryBookEntry lbEntry)
+ {
+ LiberateClicked?.Invoke(this, lbEntry.LibraryBook);
+ }
+ }
+
+ public void CloseImageDisplay()
+ {
+ if (imageDisplayDialog is not null && imageDisplayDialog.IsVisible)
+ imageDisplayDialog.Close();
+ }
+
+ public void Version_DoubleClick(object sender, Avalonia.Input.TappedEventArgs args)
+ {
+ if (sender is Control panel && panel.DataContext is ILibraryBookEntry lbe && lbe.LastDownload.IsValid)
+ lbe.LastDownload.OpenReleaseUrl();
+ }
+
+ public void Cover_Click(object sender, Avalonia.Input.TappedEventArgs args)
+ {
+ if (sender is not Image tblock || tblock.DataContext is not IGridEntry gEntry)
+ return;
+
+ if (imageDisplayDialog is null || !imageDisplayDialog.IsVisible)
+ {
+ imageDisplayDialog = new ImageDisplayDialog();
+ }
+
+ var picDef = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
+
+ void PictureCached(object sender, PictureCachedEventArgs e)
+ {
+ if (e.Definition.PictureId == picDef.PictureId)
+ imageDisplayDialog.SetCoverBytes(e.Picture);
+
+ PictureStorage.PictureCached -= PictureCached;
+ }
+
+ PictureStorage.PictureCached += PictureCached;
+ (bool isDefault, byte[] initialImageBts) = PictureStorage.GetPicture(picDef);
+
+
+ var windowTitle = $"{gEntry.Title} - Cover";
+
+
+ imageDisplayDialog.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(gEntry.LibraryBook);
+ imageDisplayDialog.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(gEntry.LibraryBook, ".jpg"));
+ imageDisplayDialog.Title = windowTitle;
+ imageDisplayDialog.SetCoverBytes(initialImageBts);
+
+ if (!isDefault)
+ PictureStorage.PictureCached -= PictureCached;
+
+ if (imageDisplayDialog.IsVisible)
+ imageDisplayDialog.Activate();
+ else
+ imageDisplayDialog.Show();
+ }
+
+ public void Description_Click(object sender, Avalonia.Input.TappedEventArgs args)
+ {
+ if (sender is Control tblock && tblock.DataContext is IGridEntry gEntry)
+ {
+ var pt = tblock.PointToScreen(tblock.Bounds.TopRight);
+ var displayWindow = new DescriptionDisplayDialog
+ {
+ SpawnLocation = new Point(pt.X, pt.Y),
+ DescriptionText = gEntry.Description,
+ };
+
+ void CloseWindow(object o, DataGridRowEventArgs e)
+ {
+ displayWindow.Close();
+ }
+ productsGrid.LoadingRow += CloseWindow;
+ displayWindow.Closing += (_, _) =>
+ {
+ productsGrid.LoadingRow -= CloseWindow;
+ };
+
+ displayWindow.Show();
+ }
+ }
+
+ BookDetailsDialog bookDetailsForm;
+
+ public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
+ {
+ var button = args.Source as Button;
+
+ if (button.DataContext is ILibraryBookEntry lbEntry && VisualRoot is Window window)
+ {
+ if (bookDetailsForm is null || !bookDetailsForm.IsVisible)
+ {
+ bookDetailsForm = new BookDetailsDialog(lbEntry.LibraryBook);
+ bookDetailsForm.Show(window);
+ }
+ else
+ bookDetailsForm.LibraryBook = lbEntry.LibraryBook;
+ }
+ }
+
+ private async void AddToPlaylistButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ var gridEntry = (IGridEntry)((Button)sender).DataContext;
+ if (gridEntry is ISeriesEntry se)
+ {
+ foreach (ILibraryBookEntry child in se.Children)
+ await _playerViewModel.AddToPlaylist(child);
+ }
+ else if (gridEntry is ILibraryBookEntry book)
+ await _playerViewModel.AddToPlaylist(book);
+ else
+ Serilog.Log.Logger.TryLogError($"Cannot add item type {gridEntry.GetType().Name} to playlist");
+ }
+ #endregion
+ }
}
diff --git a/Source/LibationAvalonia/Views/SidebarControl.axaml b/Source/LibationAvalonia/Views/SidebarControl.axaml
new file mode 100644
index 000000000..37fd52ea3
--- /dev/null
+++ b/Source/LibationAvalonia/Views/SidebarControl.axaml
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Process Queue
+
+
+
+
+
+
+
+
+
+ Cancel All
+
+
+
+
+
+
+
+
+
+ Clear Finished
+
+
+
+
+
+
+
+
+ Queue Log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Copy Log Entries to Clipboard
+
+
+ Clear Log
+
+
+
+
+
+
+
+
+ Playlist
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Copy Log Entries to Clipboard
+
+
+ Clear Log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/LibationAvalonia/Views/SidebarControl.axaml.cs b/Source/LibationAvalonia/Views/SidebarControl.axaml.cs
new file mode 100644
index 000000000..b3d9b5f85
--- /dev/null
+++ b/Source/LibationAvalonia/Views/SidebarControl.axaml.cs
@@ -0,0 +1,189 @@
+using ApplicationServices;
+using Avalonia.Controls;
+using Avalonia.Data.Converters;
+using DataLayer;
+using LibationAvalonia.ViewModels;
+using LibationUiBase;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace LibationAvalonia.Views
+{
+ public partial class SidebarControl : UserControl
+ {
+ private TrackedQueue Queue => _processQueue.Queue;
+ private ProcessQueueViewModel _processQueue = ServiceLocator.Get();
+
+ public SidebarControl()
+ {
+ InitializeComponent();
+
+ ProcessBookControl.PositionButtonClicked += ProcessBookControl2_ButtonClicked;
+ ProcessBookControl.CancelButtonClicked += ProcessBookControl2_CancelButtonClicked;
+
+ #region Design Mode Testing
+ if (Design.IsDesignMode)
+ {
+ var vm = ServiceLocator.Get();
+ var Logger = LogMe.RegisterForm(vm);
+ DataContext = vm;
+ using var context = DbContexts.GetContext();
+ List testList = new()
+ {
+ new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
+ {
+ Result = ProcessBookResult.FailedAbort,
+ Status = ProcessBookStatus.Failed,
+ },
+ new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), Logger)
+ {
+ Result = ProcessBookResult.FailedSkip,
+ Status = ProcessBookStatus.Failed,
+ },
+ new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), Logger)
+ {
+ Result = ProcessBookResult.FailedRetry,
+ Status = ProcessBookStatus.Failed,
+ },
+ new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), Logger)
+ {
+ Result = ProcessBookResult.ValidationFail,
+ Status = ProcessBookStatus.Failed,
+ },
+ new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), Logger)
+ {
+ Result = ProcessBookResult.Cancelled,
+ Status = ProcessBookStatus.Cancelled,
+ },
+ new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), Logger)
+ {
+ Result = ProcessBookResult.Success,
+ Status = ProcessBookStatus.Completed,
+ },
+ new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), Logger)
+ {
+ Result = ProcessBookResult.None,
+ Status = ProcessBookStatus.Working,
+ },
+ new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
+ {
+ Result = ProcessBookResult.None,
+ Status = ProcessBookStatus.Queued,
+ },
+ };
+
+ vm.Queue.Enqueue(testList);
+ vm.Queue.MoveNext();
+ vm.Queue.MoveNext();
+ vm.Queue.MoveNext();
+ vm.Queue.MoveNext();
+ vm.Queue.MoveNext();
+ vm.Queue.MoveNext();
+ vm.Queue.MoveNext();
+ return;
+ }
+ #endregion
+ }
+
+ public void NumericUpDown_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
+ {
+ if (e.Key == Avalonia.Input.Key.Enter && sender is Avalonia.Input.IInputElement input) input.Focus();
+ }
+
+ #region Control event handlers
+
+ private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item)
+ {
+ if (item is not null)
+ await item.CancelAsync();
+ Queue.RemoveQueued(item);
+ }
+
+ private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel item, QueuePosition queueButton)
+ {
+ Queue.MoveQueuePosition(item, queueButton);
+ }
+
+ public async void CancelAllBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ Queue.ClearQueue();
+ if (Queue.Current is not null)
+ await Queue.Current.CancelAsync();
+ }
+
+ public void ClearFinishedBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ Queue.ClearCompleted();
+
+ if (!_processQueue.Running)
+ _processQueue.RunningTime = string.Empty;
+ }
+
+ public void ClearLogBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ _processQueue.LogEntries.Clear();
+ }
+
+ private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ string logText = string.Join("\r\n", _processQueue.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}"));
+ await App.MainWindow.Clipboard.SetTextAsync(logText);
+ }
+
+ private async void cancelAllBtn_Click(object sender, EventArgs e)
+ {
+ Queue.ClearQueue();
+ if (Queue.Current is not null)
+ await Queue.Current.CancelAsync();
+ }
+
+ private void btnClearFinished_Click(object sender, EventArgs e)
+ {
+ Queue.ClearCompleted();
+
+ if (!_processQueue.Running)
+ _processQueue.RunningTime = string.Empty;
+ }
+
+ #endregion
+ }
+
+ public class DecimalConverter : IValueConverter
+ {
+ public static readonly DecimalConverter Instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string sourceText && targetType.IsAssignableTo(typeof(decimal?)))
+ {
+ if (sourceText == "∞") return 0;
+
+ for (int i = sourceText.Length; i > 0; i--)
+ {
+ if (decimal.TryParse(sourceText[..i], out var val))
+ return val;
+ }
+
+ return 0;
+ }
+ return 0;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is decimal val)
+ {
+ return
+ val == 0 ? "∞"
+ : (
+ val >= 10 ? ((long)val).ToString()
+ : val >= 1 ? val.ToString("F1")
+ : val.ToString("F2")
+ ) + " MB/s";
+ }
+ return value?.ToString();
+ }
+ }
+}
diff --git a/Source/LibationAvalonia/WpfCanExecuteChanged.cs b/Source/LibationAvalonia/WpfCanExecuteChanged.cs
new file mode 100644
index 000000000..116faf7c2
--- /dev/null
+++ b/Source/LibationAvalonia/WpfCanExecuteChanged.cs
@@ -0,0 +1,21 @@
+using Avalonia.Labs.Input;
+using Avalonia.Threading;
+using LibationUiBase.ViewModels;
+using System;
+
+namespace LibationAvalonia
+{
+ public class WpfCanExecuteChanged : ICanExecuteChanged
+ {
+ public event EventHandler Event
+ {
+ add => CommandManager.RequerySuggested += value;
+ remove => CommandManager.RequerySuggested -= value;
+ }
+
+ public void Raise()
+ {
+ Dispatcher.UIThread.Post(CommandManager.InvalidateRequerySuggested);
+ }
+ }
+}
diff --git a/Source/LibationUiBase/BindingListExtensions.cs b/Source/LibationUiBase/BindingListExtensions.cs
new file mode 100644
index 000000000..93c5a5942
--- /dev/null
+++ b/Source/LibationUiBase/BindingListExtensions.cs
@@ -0,0 +1,28 @@
+using AppScaffolding;
+using System.Collections;
+
+namespace LibationUiBase;
+
+public static class BindingListExtensions
+{
+ public static void Swap(this IList list, int first, int second)
+ {
+ (list[first], list[second]) = (list[second], list[first]);
+ }
+
+ public static void MoveDown(this IList list, object obj)
+ {
+ var idx = list.IndexOf(obj);
+ Ensure.IsValid(nameof(obj), idx != -1, "Object not in list");
+ Ensure.IsValid(nameof(obj), idx < list.Count - 1, "Object already at end of list");
+ list.Swap(idx, idx + 1);
+ }
+
+ public static void MoveUp(this IList list, object obj)
+ {
+ var idx = list.IndexOf(obj);
+ Ensure.IsValid(nameof(obj), idx != -1, "Object not in list");
+ Ensure.IsValid(nameof(obj), idx > 0, "Object already at beginning of list");
+ list.Swap(idx, idx - 1);
+ }
+}
\ No newline at end of file
diff --git a/Source/LibationUiBase/GridView/GridEntry[TStatus].cs b/Source/LibationUiBase/GridView/GridEntry[TStatus].cs
index f8ef11d42..7b01e4056 100644
--- a/Source/LibationUiBase/GridView/GridEntry[TStatus].cs
+++ b/Source/LibationUiBase/GridView/GridEntry[TStatus].cs
@@ -4,6 +4,7 @@
using Dinah.Core.Threading;
using FileLiberator;
using LibationFileManager;
+using LibationUiBase.ViewModels.Player;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -14,7 +15,7 @@
namespace LibationUiBase.GridView
{
- public enum RemoveStatus
+ public enum RemoveStatus
{
NotRemoved,
Removed,
@@ -24,7 +25,9 @@ public enum RemoveStatus
/// The View Model base for the DataGridView
public abstract class GridEntry : SynchronizeInvoker, IGridEntry where TStatus : IEntryStatus
{
- [Browsable(false)] public string AudibleProductId => Book.AudibleProductId;
+ private PlayerViewModel _player = ServiceLocator.Get();
+
+ [Browsable(false)] public string AudibleProductId => Book.AudibleProductId;
[Browsable(false)] public LibraryBook LibraryBook { get; protected set; }
[Browsable(false)] public float SeriesIndex { get; protected set; }
[Browsable(false)] public abstract DateTime DateAdded { get; }
@@ -123,7 +126,18 @@ public void UpdateLibraryBook(LibraryBook libraryBook)
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
- protected abstract string GetBookTags();
+ public virtual bool CanAddToPlaylistText => this is not ISeriesEntry;
+ public virtual string AddToPlaylistText
+ {
+ get
+ {
+ return this is ISeriesEntry book ? null : BookIsInPlaylist ? "Add to Playlist" : "Remove from Playlist";
+ }
+ }
+
+ public bool BookIsInPlaylist => this is ILibraryBookEntry book && _player.IsInPlaylist(book);
+
+ protected abstract string GetBookTags();
protected virtual DateTime GetPurchaseDate() => LibraryBook.DateAdded;
protected virtual int GetLengthInMinutes() => Book.LengthInMinutes;
protected string GetPurchaseDateString() => GetPurchaseDate().ToString("d");
diff --git a/Source/LibationUiBase/GridView/IGridEntry.cs b/Source/LibationUiBase/GridView/IGridEntry.cs
index ed3ebb9ac..9f810a750 100644
--- a/Source/LibationUiBase/GridView/IGridEntry.cs
+++ b/Source/LibationUiBase/GridView/IGridEntry.cs
@@ -5,7 +5,7 @@
namespace LibationUiBase.GridView
{
- public interface IGridEntry : IMemberComparable, INotifyPropertyChanged
+ public interface IGridEntry : IMemberComparable, INotifyPropertyChanged
{
EntryStatus Liberate { get; }
float SeriesIndex { get; }
@@ -30,5 +30,11 @@ public interface IGridEntry : IMemberComparable, INotifyPropertyChanged
Rating MyRating { get; set; }
string BookTags { get; }
void UpdateLibraryBook(LibraryBook libraryBook);
- }
+
+ bool CanAddToPlaylistText { get; }
+ string AddToPlaylistText { get; }
+ bool BookIsInPlaylist { get; }
+
+ //int? CurrentChapterNumber { get; }
+ }
}
diff --git a/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs b/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs
index 5866ae409..52038e316 100644
--- a/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs
+++ b/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs
@@ -68,6 +68,10 @@ public static async Task> GetAllProductsAsync(IEnumerable a).ToList();
}
- protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
- }
+ public override string AddToPlaylistText => BookIsInPlaylist ? "Remove From Playlist" : "Add to Playlist";
+
+ protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
+
+ public override string ToString() => $"{AudibleProductId} - {Title}";
+ }
}
diff --git a/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs b/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs
index 0c140946d..eebc71eec 100644
--- a/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs
+++ b/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs
@@ -11,10 +11,10 @@ namespace LibationUiBase.GridView
/// The View Model for a LibraryBook that is ContentType.Parent
public class SeriesEntry : GridEntry, ISeriesEntry where TStatus : IEntryStatus
{
- public List Children { get; }
+ public List Children { get; }
public override DateTime DateAdded => Children.Max(c => c.DateAdded);
- private bool suspendCounting = false;
+ private bool suspendCounting;
public void ChildRemoveUpdate()
{
if (suspendCounting) return;
@@ -42,7 +42,7 @@ public override bool? Remove
}
}
- public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent, new[] { child }) { }
+ public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent, [child]) { }
public SeriesEntry(LibraryBook parent, IEnumerable children)
{
LastDownload = new();
@@ -118,7 +118,7 @@ public void RemoveChild(ILibraryBookEntry lbe)
Length = GetBookLengthString();
}
- protected override string GetBookTags() => null;
+ protected override string GetBookTags() => null;
protected override int GetLengthInMinutes() => Children.Count == 0 ? 0 : Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
protected override DateTime GetPurchaseDate() => Children.Count == 0 ? default : Children.Min(c => c.LibraryBook.DateAdded);
}
diff --git a/Source/LibationUiBase/LibationUiBase.csproj b/Source/LibationUiBase/LibationUiBase.csproj
index ce4a3b512..ca4f9b228 100644
--- a/Source/LibationUiBase/LibationUiBase.csproj
+++ b/Source/LibationUiBase/LibationUiBase.csproj
@@ -9,6 +9,7 @@
+
diff --git a/Source/LibationUiBase/RelayCommand.cs b/Source/LibationUiBase/RelayCommand.cs
new file mode 100644
index 000000000..e3217162d
--- /dev/null
+++ b/Source/LibationUiBase/RelayCommand.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Windows.Input;
+using LibationUiBase.ViewModels;
+
+namespace LibationUiBase;
+
+public class RelayCommand : ICommand
+{
+ private Action execute;
+ private Func canExecute;
+ private ICanExecuteChanged canExecuteChanged;
+
+ public event EventHandler CanExecuteChanged
+ {
+ add { canExecuteChanged.Event += value; }
+ remove { canExecuteChanged.Event -= value; }
+ }
+
+ public RelayCommand(Action execute, Func canExecute = null)
+ {
+ this.execute = execute;
+ this.canExecute = canExecute;
+ canExecuteChanged = ServiceLocator.Get();
+ }
+
+ public bool CanExecute(object parameter)
+ {
+ return canExecute == null || canExecute(parameter);
+ }
+
+ public void Execute(object parameter)
+ {
+ execute(parameter);
+ }
+
+ public void RaiseCanExecuteChanged()
+ {
+ canExecuteChanged.Raise();
+ }
+}
\ No newline at end of file
diff --git a/Source/LibationUiBase/ServiceLocator.cs b/Source/LibationUiBase/ServiceLocator.cs
new file mode 100644
index 000000000..b825f22a3
--- /dev/null
+++ b/Source/LibationUiBase/ServiceLocator.cs
@@ -0,0 +1,140 @@
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Linq;
+using Prism.Events;
+
+namespace LibationUiBase;
+
+public static class ServiceLocator
+{
+ private static readonly ServiceCollection MutableContainer = new();
+ private static ServiceProvider _container;
+
+ ///
+ /// Registers a type that when requested, will always return the same, single instance.
+ ///
+ ///
+ public static void RegisterSingleton(Type t)
+ {
+ CheckContainerNotBuilt();
+ if (!TypeExists(t))
+ MutableContainer.AddSingleton(t);
+ }
+
+ ///
+ /// Registers a type that when requested, will always return the same, single instance.
+ ///
+ public static void RegisterSingleton() where T : class
+ {
+ CheckContainerNotBuilt();
+ if (!TypeExists(typeof(T)))
+ MutableContainer.AddSingleton(typeof(T));
+ }
+
+ ///
+ /// Registers a type that when requested, will always return the same, single instance.
+ ///
+ public static void RegisterSingleton()
+ where TInterface : class
+ where TImplementation : TInterface
+ {
+ CheckContainerNotBuilt();
+ if (!TypeExists(typeof(TInterface)))
+ MutableContainer.AddSingleton(typeof(TInterface), typeof(TImplementation));
+ }
+
+
+ ///
+ /// Registers a singleton but using a certain instance.
+ ///
+ ///
+ public static void RegisterInstance(object obj)
+ where TInterface : class
+ where TImplementation : TInterface
+ {
+ CheckContainerNotBuilt();
+ if (!TypeExists(typeof(TInterface)))
+ MutableContainer.AddSingleton(_ => (TImplementation)obj);
+ }
+
+ ///
+ /// Registers a type that gets a new instance every
+ /// call to Get().
+ ///
+ ///
+ public static void RegisterTransient(Type t)
+ {
+ CheckContainerNotBuilt();
+ if (!TypeExists(t))
+ MutableContainer.AddTransient(t);
+ }
+
+ ///
+ /// Registers a type that gets a new instance every
+ /// call to Get().
+ ///
+ public static void RegisterTransient()
+ {
+ CheckContainerNotBuilt();
+ if (!TypeExists(typeof(T)))
+ MutableContainer.AddTransient(typeof(T));
+ }
+
+ public static void RegisterTransient()
+ {
+ CheckContainerNotBuilt();
+ if (!TypeExists(typeof(TInterface)))
+ MutableContainer.AddTransient(typeof(TInterface), typeof(TImplementation));
+ }
+
+
+
+ ///
+ /// Call once at app startup after add all required types.
+ ///
+ public static void AddCommonServicesAndBuild()
+ {
+ CheckContainerNotBuilt();
+
+ // Add common, cross-platform services here.
+ RegisterSingleton();
+
+ // Finalize and build container, mutableContainer should
+ // no longer be used.
+ _container = MutableContainer.BuildServiceProvider();
+ }
+
+ public static T Get()
+ {
+ CheckContainerBuilt();
+ return _container.GetService();
+ }
+
+ public static object Get(Type type)
+ {
+ CheckContainerBuilt();
+ if (!TypeExists(type))
+ throw new InvalidOperationException($"Type {type.Name} has not been registered.");
+ return _container.GetService(type);
+ }
+
+ private static bool TypeExists(Type type)
+ {
+ var exists = MutableContainer.Any(sd => sd.ServiceType == type);
+ if (exists)
+ Serilog.Log.Logger.Warning($"Type already registered: {type.Name}");
+ return exists;
+ }
+
+ private static void CheckContainerNotBuilt()
+ {
+ if (_container != null)
+ throw new Exception("Container already built!");
+ }
+
+ private static void CheckContainerBuilt()
+ {
+ if (_container == null)
+ throw new Exception("Container not yet built!");
+ }
+}
\ No newline at end of file
diff --git a/Source/LibationUiBase/ViewModels/ICanExecuteChanged.cs b/Source/LibationUiBase/ViewModels/ICanExecuteChanged.cs
new file mode 100644
index 000000000..51b804666
--- /dev/null
+++ b/Source/LibationUiBase/ViewModels/ICanExecuteChanged.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace LibationUiBase.ViewModels;
+
+public interface ICanExecuteChanged
+{
+ event EventHandler Event;
+ void Raise();
+}
\ No newline at end of file
diff --git a/Source/LibationUiBase/ViewModels/Player/PlayerEvents.cs b/Source/LibationUiBase/ViewModels/Player/PlayerEvents.cs
new file mode 100644
index 000000000..9a95d5cc7
--- /dev/null
+++ b/Source/LibationUiBase/ViewModels/Player/PlayerEvents.cs
@@ -0,0 +1,27 @@
+using LibationUiBase.GridView;
+using Prism.Events;
+
+namespace LibationUiBase.ViewModels.Player
+{
+ public class BookAddedToPlaylist : PubSubEvent
+ {
+ public ILibraryBookEntry Book { get; }
+
+ public BookAddedToPlaylist() {}
+ public BookAddedToPlaylist(ILibraryBookEntry book)
+ {
+ Book = book;
+ }
+ }
+
+ public class BookRemovedFromPlaylist : PubSubEvent
+ {
+ public ILibraryBookEntry Book { get; }
+
+ public BookRemovedFromPlaylist() {}
+ public BookRemovedFromPlaylist(ILibraryBookEntry book)
+ {
+ Book = book;
+ }
+ }
+}
diff --git a/Source/LibationUiBase/ViewModels/Player/PlayerViewModel.cs b/Source/LibationUiBase/ViewModels/Player/PlayerViewModel.cs
new file mode 100644
index 000000000..76e137f35
--- /dev/null
+++ b/Source/LibationUiBase/ViewModels/Player/PlayerViewModel.cs
@@ -0,0 +1,123 @@
+using LibationUiBase.GridView;
+using Prism.Events;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace LibationUiBase.ViewModels.Player;
+
+public class PlayerViewModel : ViewModelBase
+{
+ private readonly IEventAggregator eventAggregator;
+ private BindingList playlistItems = new();
+ ///
+ /// Public for data binding reasons. Do NOT manipulate directly.
+ ///
+ public BindingList PlaylistItems
+ {
+ get => playlistItems;
+ set => RaiseAndSetIfChanged(ref playlistItems, value);
+ }
+
+ private PlaylistEntryViewModel selectedBook;
+ public PlaylistEntryViewModel SelectedBook
+ {
+ get => selectedBook;
+ set => RaiseAndSetIfChanged(ref selectedBook, value);
+ }
+
+ public RelayCommand MoveUpCommand;
+ public RelayCommand MoveDownCommand;
+
+ public PlayerViewModel(IEventAggregator eventAggregator)
+ {
+ MoveUpCommand = new RelayCommand(_ =>
+ {
+ playlistItems.MoveUp(SelectedBook);
+ RenumberPlaylist();
+ var temp = SelectedBook;
+ SelectedBook = null;
+ SelectedBook = temp;
+ }, _ => SelectedBook != null && playlistItems.IndexOf(SelectedBook) > 0 &&
+ IsInPlaylist(SelectedBook));
+
+ MoveDownCommand = new RelayCommand(_ =>
+ {
+ playlistItems.MoveDown(SelectedBook);
+ RenumberPlaylist();
+ var temp = SelectedBook;
+ SelectedBook = null;
+ SelectedBook = temp;
+ }, _ => SelectedBook != null && playlistItems.IndexOf(SelectedBook) < playlistItems.Count - 1 &&
+ IsInPlaylist(SelectedBook));
+
+ this.eventAggregator = eventAggregator;
+ }
+
+ private void RenumberPlaylist()
+ {
+ for (var i = 0; i < playlistItems.Count; i++)
+ playlistItems[i].Sequence = i + 1;
+ }
+
+ public async ValueTask AddToPlaylist(ILibraryBookEntry book)
+ {
+ var plevm = ServiceLocator.Get();
+ await plevm.Init(book);
+ plevm.Sequence = playlistItems.Count + 1;
+ playlistItems.Add(plevm);
+ //eventAggregator.GetEvent().Publish(book);
+ }
+
+ public ValueTask RemoveFromPlaylist(ILibraryBookEntry book)
+ {
+ var plevm = playlistItems.FirstOrDefault(b => b.BookEntry.AudibleProductId == book.Book.AudibleProductId);
+ if (plevm != null)
+ {
+ playlistItems.Remove(plevm);
+ InvalidateCommands();
+ //eventAggregator.GetEvent().Publish(book);
+ }
+
+ return ValueTask.CompletedTask;
+ }
+
+ private void InvalidateCommands()
+ {
+ MoveDownCommand.RaiseCanExecuteChanged();
+ MoveUpCommand.RaiseCanExecuteChanged();
+ }
+
+ public bool IsInPlaylist(ILibraryBookEntry book) =>
+ PlaylistItems.Any(item => item.BookEntry.AudibleProductId == book.AudibleProductId);
+
+ public bool IsInPlaylist(PlaylistEntryViewModel book) =>
+ PlaylistItems.Any(item => item.BookEntry.AudibleProductId == book.BookEntry.AudibleProductId);
+
+
+ public override string ToString() => $"{nameof(PlayerViewModel)}: {string.Join(", ", playlistItems)}";
+
+ protected override void OnPropertyChanging(string propertyName)
+ {
+ switch (propertyName)
+ {
+ case nameof(SelectedBook):
+ if (SelectedBook != null)
+ SelectedBook.IsCurrent = false;
+ InvalidateCommands();
+ break;
+ }
+ }
+
+ protected override void OnPropertyChanged(string propertyName)
+ {
+ switch (propertyName)
+ {
+ case nameof(SelectedBook):
+ if (SelectedBook != null)
+ SelectedBook.IsCurrent = true;
+ InvalidateCommands();
+ break;
+ }
+ }
+}
diff --git a/Source/LibationUiBase/ViewModels/Player/PlaylistEntryViewModel.cs b/Source/LibationUiBase/ViewModels/Player/PlaylistEntryViewModel.cs
new file mode 100644
index 000000000..eadb180dc
--- /dev/null
+++ b/Source/LibationUiBase/ViewModels/Player/PlaylistEntryViewModel.cs
@@ -0,0 +1,72 @@
+using LibationUiBase.GridView;
+using System.Threading.Tasks;
+
+namespace LibationUiBase.ViewModels.Player;
+
+public class PlaylistEntryViewModel : ViewModelBase
+{
+ const string CurrentIndicator = "▶";
+
+ private int sequence;
+ public int Sequence
+ {
+ get => sequence;
+ set => RaiseAndSetIfChanged(ref sequence, value);
+ }
+
+ private string seriesName = string.Empty;
+ public string SeriesName
+ {
+ get => seriesName;
+ set => RaiseAndSetIfChanged(ref seriesName, value);
+ }
+
+ public string Title => bookEntry?.Title;
+ public string Series => bookEntry?.Series;
+
+ private bool isCurrent;
+ public bool IsCurrent
+ {
+ get => isCurrent;
+ set => this.RaiseAndSetIfChanged(ref isCurrent, value);
+ }
+
+ // "►" indicator for winforms
+ private string isCurrentStr = string.Empty;
+ public string IsCurrentStr
+ {
+ get => isCurrentStr;
+ set => this.RaiseAndSetIfChanged(ref isCurrentStr, value);
+ }
+
+ private ILibraryBookEntry bookEntry;
+ public ILibraryBookEntry BookEntry
+ {
+ get => bookEntry;
+ set => RaiseAndSetIfChanged(ref bookEntry, value);
+ }
+
+ public async ValueTask Init(ILibraryBookEntry bookEntry)
+ {
+ BookEntry = bookEntry;
+ }
+
+ public override string ToString() => Title;
+
+ protected override void OnPropertyChanged(string propertyName)
+ {
+ switch (propertyName)
+ {
+ case nameof(IsCurrent):
+ IsCurrentStr = IsCurrent ? CurrentIndicator : null;
+ break;
+ }
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is PlaylistEntryViewModel plevm && bookEntry != null)
+ return bookEntry?.AudibleProductId == plevm?.bookEntry.AudibleProductId;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Source/LibationUiBase/ViewModels/ViewModelBase.cs b/Source/LibationUiBase/ViewModels/ViewModelBase.cs
new file mode 100644
index 000000000..7795bb375
--- /dev/null
+++ b/Source/LibationUiBase/ViewModels/ViewModelBase.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace LibationUiBase.ViewModels;
+
+public abstract class ViewModelBase : INotifyPropertyChanged, INotifyPropertyChanging
+{
+ public ViewModelBase()
+ {
+ this.PropertyChanged += (_, e) => OnPropertyChanged(e.PropertyName);
+ this.PropertyChanging += (_, e) => OnPropertyChanging(e.PropertyName);
+ }
+
+ protected virtual void OnPropertyChanging(string propertyName) { }
+ protected virtual void OnPropertyChanged(string propertyName) { }
+
+ public TRet RaiseAndSetIfChanged(ref TRet backingField, TRet newValue, [CallerMemberName] string propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(backingField, newValue)) return newValue;
+
+ RaisePropertyChanging(propertyName);
+ backingField = newValue;
+ RaisePropertyChanged(propertyName);
+ return newValue;
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ public void RaisePropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ public event PropertyChangingEventHandler PropertyChanging;
+ public void RaisePropertyChanging(string propertyName) => PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
+}
+
diff --git a/Source/LibationWinForms/Form1.Designer.cs b/Source/LibationWinForms/Form1.Designer.cs
index 0c1473894..7da7de496 100644
--- a/Source/LibationWinForms/Form1.Designer.cs
+++ b/Source/LibationWinForms/Form1.Designer.cs
@@ -85,7 +85,7 @@ private void InitializeComponent()
this.toggleQueueHideBtn = new System.Windows.Forms.Button();
this.doneRemovingBtn = new System.Windows.Forms.Button();
this.removeBooksBtn = new System.Windows.Forms.Button();
- this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl();
+ this.processBookQueue1 = new LibationWinForms.ProcessQueue.SidebarControl();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
@@ -691,7 +691,7 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStripMenuItem launchHangoverToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
private System.Windows.Forms.SplitContainer splitContainer1;
- private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1;
+ private LibationWinForms.ProcessQueue.SidebarControl processBookQueue1;
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Button toggleQueueHideBtn;
public LibationWinForms.GridView.ProductsDisplay productsDisplay;
diff --git a/Source/LibationWinForms/GridView/DataGridViewEx.cs b/Source/LibationWinForms/GridView/DataGridViewEx.cs
new file mode 100644
index 000000000..285407af0
--- /dev/null
+++ b/Source/LibationWinForms/GridView/DataGridViewEx.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq.Expressions;
+using System.Windows.Forms;
+
+namespace LibationWinForms.GridView;
+
+///
+/// A DataGridView with a bindable SelectedItem property.
+///
+public class DataGridViewEx : DataGridView, INotifyPropertyChanged
+{
+ private BindingSource bindingSource;
+
+ private object selectedItem;
+ ///
+ /// Can bind to this.
+ ///
+ public object SelectedItem
+ {
+ get => DbNullToNull(selectedItem);
+ set => SetCurrentItem(DbNullToNull(value));
+ }
+
+ public DataGridViewEx()
+ {
+ this.DefaultCellStyle.DataSourceNullValue = null;
+ EditMode = DataGridViewEditMode.EditProgrammatically;
+ MultiSelect = false;
+ ColumnHeadersDefaultCellStyle.SelectionBackColor =
+ ColumnHeadersDefaultCellStyle.BackColor;
+ SelectionMode = DataGridViewSelectionMode.FullRowSelect;
+ this.DataSourceChanged += DataGridViewEx_DataSourceChanged;
+ }
+
+ public void AddSelectedItemBinding(T vm, Expression> selectedItemProperty)
+ {
+ try
+ {
+ var prop = GetPropertyName(selectedItemProperty);
+ this.DataBindings.Add(nameof(SelectedItem), vm, prop);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+ }
+
+ static string GetPropertyName(Expression> propertyExpression)
+ {
+ var memberExpression = propertyExpression.Body as MemberExpression;
+
+ if (memberExpression == null)
+ {
+ if (propertyExpression.Body is UnaryExpression unaryExpression)
+ {
+ memberExpression = unaryExpression.Operand as MemberExpression;
+ }
+ }
+
+ if (memberExpression != null)
+ return memberExpression.Member.Name;
+
+ throw new ArgumentException($"Property expression is not a member access: {propertyExpression}", nameof(propertyExpression));
+ }
+
+ private void DataGridViewEx_DataSourceChanged(object sender, EventArgs e)
+ {
+ try
+ {
+ // Remove events from old binding source.
+ if (this.bindingSource is not null)
+ {
+ this.bindingSource.CurrencyManager.PositionChanged -= CurrencyManager_PositionChanged;
+ this.bindingSource.CurrencyManager.ListChanged -= CurrencyManager_ListChanged;
+ }
+
+ if (DataSource is BindingSource bs)
+ {
+ // Set up events on new binding source.
+ this.bindingSource = bs;
+ bs.CurrencyManager.PositionChanged += CurrencyManager_PositionChanged;
+ bs.CurrencyManager.ListChanged += CurrencyManager_ListChanged;
+ this.DataBindings.DefaultDataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged;
+ RaisePropertyChanged(nameof(SelectedItem));
+ }
+ else
+ throw new InvalidOperationException($"{nameof(DataGridViewEx)} data source must be a BindingSource in order to handle selection.");
+ }
+ catch (Exception exception)
+ {
+ Console.WriteLine(exception);
+ throw;
+ }
+ }
+
+ private void CurrencyManager_PositionChanged(object sender, EventArgs args)
+ {
+ var current = GetCurrent();
+ if (current != null && current != SelectedItem)
+ SelectedItem = current;
+ }
+
+ private void CurrencyManager_ListChanged(object sender, ListChangedEventArgs args)
+ {
+ if (args.ListChangedType == ListChangedType.Reset ||
+ args.ListChangedType == ListChangedType.ItemDeleted)
+ {
+ if (this.bindingSource.Position == -1)
+ {
+ selectedItem = null;
+ }
+ if (SelectedRows.Count > 0)
+ BeginInvoke(new MethodInvoker(() =>
+ {
+ if (SelectedRows.Count > 0)
+ SelectedItem = SelectedRows[0].DataBoundItem;
+ }));
+ }
+ }
+
+ private object GetCurrent()
+ {
+ try
+ {
+ return bindingSource.Count == 0 ||
+ bindingSource.CurrencyManager.Position == -1 ||
+ bindingSource.CurrencyManager.Position >= Rows.Count
+ ? null
+ : DbNullToNull(Rows[bindingSource.CurrencyManager.Position].DataBoundItem);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+ }
+
+ private void SetCurrentItem(object dataItem)
+ {
+ try
+ {
+ if (dataItem == null)
+ {
+ if (selectedItem != null)
+ {
+ selectedItem = null;
+ if (SelectedRows.Count > 0)
+ BeginInvoke(new MethodInvoker(ClearSelection));
+ }
+ return;
+ }
+
+ selectedItem = dataItem;
+
+ for (var index = 0; index < Rows.Count; index++)
+ {
+ var row = Rows[index];
+
+ // Change the physically selected row in the grid.
+ if (CurrentRow == null ||
+ row.DataBoundItem == dataItem)
+ {
+ BeginInvoke(new MethodInvoker(() =>
+ CurrentCell = row.Cells[0]));
+ return;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+ finally
+ {
+ RaisePropertyChanged(nameof(SelectedItem));
+ }
+ }
+
+ private object DbNullToNull(object dataItem)
+ {
+ return dataItem is DBNull ? null: dataItem;
+ }
+
+ List handlers = new();
+ public event PropertyChangedEventHandler PropertyChanged
+ {
+ add { handlers.Add(value); }
+ remove { handlers.Remove(value); }
+ }
+
+ private void RaisePropertyChanged(string propertyName)
+ {
+ try
+ {
+ var ev = new PropertyChangedEventArgs(propertyName);
+ foreach (var handler in handlers)
+ handler(this, ev);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ // Eat DBNull conversion bug.
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs b/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs
index d3516228c..0290eb7c9 100644
--- a/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs
+++ b/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs
@@ -22,292 +22,322 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}
- #region Component Designer generated code
+ #region Component Designer generated code
- ///
- /// Required method for Designer support - do not modify
- /// the contents of this method with the code editor.
- ///
- private void InitializeComponent()
- {
- components = new System.ComponentModel.Container();
- System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
- System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
- gridEntryDataGridView = new System.Windows.Forms.DataGridView();
- removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
- liberateGVColumn = new LiberateDataGridViewImageButtonColumn();
- coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
- titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- authorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- narratorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- lengthGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- seriesGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- seriesOrderGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- descriptionGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- categoryGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- productRatingGVColumn = new MyRatingGridViewColumn();
- purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- myRatingGVColumn = new MyRatingGridViewColumn();
- miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
- lastDownloadedGVColumn = new LastDownloadedGridViewColumn();
- tagAndDetailsGVColumn = new EditTagsDataGridViewImageButtonColumn();
- showHideColumnsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
- syncBindingSource = new SyncBindingSource(components);
- ((System.ComponentModel.ISupportInitialize)gridEntryDataGridView).BeginInit();
- ((System.ComponentModel.ISupportInitialize)syncBindingSource).BeginInit();
- SuspendLayout();
- //
- // gridEntryDataGridView
- //
- gridEntryDataGridView.AllowUserToAddRows = false;
- gridEntryDataGridView.AllowUserToDeleteRows = false;
- gridEntryDataGridView.AllowUserToOrderColumns = true;
- gridEntryDataGridView.AllowUserToResizeRows = false;
- gridEntryDataGridView.AutoGenerateColumns = false;
- gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { removeGVColumn, liberateGVColumn, coverGVColumn, titleGVColumn, authorsGVColumn, narratorsGVColumn, lengthGVColumn, seriesGVColumn, seriesOrderGVColumn, descriptionGVColumn, categoryGVColumn, productRatingGVColumn, purchaseDateGVColumn, myRatingGVColumn, miscGVColumn, lastDownloadedGVColumn, tagAndDetailsGVColumn });
- gridEntryDataGridView.ContextMenuStrip = showHideColumnsContextMenuStrip;
- gridEntryDataGridView.DataSource = syncBindingSource;
- dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
- dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window;
- dataGridViewCellStyle2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
- dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText;
- dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight;
- dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
- dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
- gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle2;
- gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
- gridEntryDataGridView.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter;
- gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
- gridEntryDataGridView.Margin = new System.Windows.Forms.Padding(6);
- gridEntryDataGridView.Name = "gridEntryDataGridView";
- gridEntryDataGridView.RowHeadersVisible = false;
- gridEntryDataGridView.RowHeadersWidth = 82;
- gridEntryDataGridView.RowTemplate.Height = 82;
- gridEntryDataGridView.Size = new System.Drawing.Size(3140, 760);
- gridEntryDataGridView.TabIndex = 0;
- gridEntryDataGridView.CellContentClick += DataGridView_CellContentClick;
- gridEntryDataGridView.CellToolTipTextNeeded += gridEntryDataGridView_CellToolTipTextNeeded;
- //
- // removeGVColumn
- //
- removeGVColumn.DataPropertyName = "Remove";
- removeGVColumn.FalseValue = "";
- removeGVColumn.Frozen = true;
- removeGVColumn.HeaderText = "Remove";
- removeGVColumn.IndeterminateValue = "";
- removeGVColumn.MinimumWidth = 60;
- removeGVColumn.Name = "removeGVColumn";
- removeGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
- removeGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
- removeGVColumn.ThreeState = true;
- removeGVColumn.TrueValue = "";
- removeGVColumn.Width = 60;
- //
- // liberateGVColumn
- //
- liberateGVColumn.DataPropertyName = "Liberate";
- liberateGVColumn.HeaderText = "Liberate";
- liberateGVColumn.MinimumWidth = 10;
- liberateGVColumn.Name = "liberateGVColumn";
- liberateGVColumn.ReadOnly = true;
- liberateGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
- liberateGVColumn.ScaleFactor = 0F;
- liberateGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
- liberateGVColumn.Width = 75;
- //
- // coverGVColumn
- //
- coverGVColumn.DataPropertyName = "Cover";
- coverGVColumn.HeaderText = "Cover";
- coverGVColumn.ImageLayout = System.Windows.Forms.DataGridViewImageCellLayout.Zoom;
- coverGVColumn.MinimumWidth = 10;
- coverGVColumn.Name = "coverGVColumn";
- coverGVColumn.ReadOnly = true;
- coverGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
- coverGVColumn.ToolTipText = "Cover Art";
- coverGVColumn.Width = 80;
- //
- // titleGVColumn
- //
- titleGVColumn.DataPropertyName = "Title";
- titleGVColumn.HeaderText = "Title";
- titleGVColumn.MinimumWidth = 10;
- titleGVColumn.Name = "titleGVColumn";
- titleGVColumn.ReadOnly = true;
- titleGVColumn.Width = 200;
- //
- // authorsGVColumn
- //
- authorsGVColumn.DataPropertyName = "Authors";
- authorsGVColumn.HeaderText = "Authors";
- authorsGVColumn.MinimumWidth = 10;
- authorsGVColumn.Name = "authorsGVColumn";
- authorsGVColumn.ReadOnly = true;
- authorsGVColumn.Width = 100;
- //
- // narratorsGVColumn
- //
- narratorsGVColumn.DataPropertyName = "Narrators";
- narratorsGVColumn.HeaderText = "Narrators";
- narratorsGVColumn.MinimumWidth = 10;
- narratorsGVColumn.Name = "narratorsGVColumn";
- narratorsGVColumn.ReadOnly = true;
- narratorsGVColumn.Width = 100;
- //
- // lengthGVColumn
- //
- lengthGVColumn.DataPropertyName = "Length";
- lengthGVColumn.HeaderText = "Length";
- lengthGVColumn.MinimumWidth = 10;
- lengthGVColumn.Name = "lengthGVColumn";
- lengthGVColumn.ReadOnly = true;
- lengthGVColumn.ToolTipText = "Recording Length";
- lengthGVColumn.Width = 100;
- //
- // seriesGVColumn
- //
- seriesGVColumn.DataPropertyName = "Series";
- seriesGVColumn.HeaderText = "Series";
- seriesGVColumn.MinimumWidth = 10;
- seriesGVColumn.Name = "seriesGVColumn";
- seriesGVColumn.ReadOnly = true;
- seriesGVColumn.Width = 100;
- //
- // seriesOrderGVColumn
- //
- seriesOrderGVColumn.DataPropertyName = "SeriesOrder";
- dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter;
- seriesOrderGVColumn.DefaultCellStyle = dataGridViewCellStyle1;
- seriesOrderGVColumn.HeaderText = "Series\r\nOrder";
- seriesOrderGVColumn.MinimumWidth = 10;
- seriesOrderGVColumn.Name = "seriesOrderGVColumn";
- seriesOrderGVColumn.ReadOnly = true;
- seriesOrderGVColumn.Width = 60;
- //
- // descriptionGVColumn
- //
- descriptionGVColumn.DataPropertyName = "Description";
- descriptionGVColumn.HeaderText = "Description";
- descriptionGVColumn.MinimumWidth = 10;
- descriptionGVColumn.Name = "descriptionGVColumn";
- descriptionGVColumn.ReadOnly = true;
- descriptionGVColumn.Width = 100;
- //
- // categoryGVColumn
- //
- categoryGVColumn.DataPropertyName = "Category";
- categoryGVColumn.HeaderText = "Category";
- categoryGVColumn.MinimumWidth = 10;
- categoryGVColumn.Name = "categoryGVColumn";
- categoryGVColumn.ReadOnly = true;
- categoryGVColumn.Width = 100;
- //
- // productRatingGVColumn
- //
- productRatingGVColumn.DataPropertyName = "ProductRating";
- productRatingGVColumn.HeaderText = "Product Rating";
- productRatingGVColumn.MinimumWidth = 10;
- productRatingGVColumn.Name = "productRatingGVColumn";
- productRatingGVColumn.ReadOnly = true;
- productRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
- productRatingGVColumn.Width = 112;
- //
- // purchaseDateGVColumn
- //
- purchaseDateGVColumn.DataPropertyName = "PurchaseDate";
- purchaseDateGVColumn.HeaderText = "Purchase Date";
- purchaseDateGVColumn.MinimumWidth = 10;
- purchaseDateGVColumn.Name = "purchaseDateGVColumn";
- purchaseDateGVColumn.ReadOnly = true;
- purchaseDateGVColumn.Width = 100;
- //
- // myRatingGVColumn
- //
- myRatingGVColumn.DataPropertyName = "MyRating";
- myRatingGVColumn.HeaderText = "My Rating";
- myRatingGVColumn.MinimumWidth = 10;
- myRatingGVColumn.Name = "myRatingGVColumn";
- myRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
- myRatingGVColumn.Width = 112;
- //
- // miscGVColumn
- //
- miscGVColumn.DataPropertyName = "Misc";
- miscGVColumn.HeaderText = "Misc";
- miscGVColumn.MinimumWidth = 10;
- miscGVColumn.Name = "miscGVColumn";
- miscGVColumn.ReadOnly = true;
- miscGVColumn.Width = 140;
- //
- // lastDownloadedGVColumn
- //
- lastDownloadedGVColumn.DataPropertyName = "LastDownload";
- lastDownloadedGVColumn.HeaderText = "Last Download";
- lastDownloadedGVColumn.MinimumWidth = 10;
- lastDownloadedGVColumn.Name = "lastDownloadedGVColumn";
- lastDownloadedGVColumn.ReadOnly = true;
- lastDownloadedGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
- lastDownloadedGVColumn.Width = 108;
- //
- // tagAndDetailsGVColumn
- //
- tagAndDetailsGVColumn.DataPropertyName = "BookTags";
- tagAndDetailsGVColumn.HeaderText = "Tags and Details";
- tagAndDetailsGVColumn.MinimumWidth = 10;
- tagAndDetailsGVColumn.Name = "tagAndDetailsGVColumn";
- tagAndDetailsGVColumn.ReadOnly = true;
- tagAndDetailsGVColumn.ScaleFactor = 0F;
- tagAndDetailsGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
- tagAndDetailsGVColumn.Width = 100;
- //
- // showHideColumnsContextMenuStrip
- //
- showHideColumnsContextMenuStrip.ImageScalingSize = new System.Drawing.Size(32, 32);
- showHideColumnsContextMenuStrip.Name = "contextMenuStrip1";
- showHideColumnsContextMenuStrip.ShowCheckMargin = true;
- showHideColumnsContextMenuStrip.Size = new System.Drawing.Size(83, 4);
- //
- // syncBindingSource
- //
- syncBindingSource.DataSource = typeof(IGridEntry);
- //
- // ProductsGrid
- //
- AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
- AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
- AutoScroll = true;
- Controls.Add(gridEntryDataGridView);
- Name = "ProductsGrid";
- Size = new System.Drawing.Size(1570, 380);
- Load += new System.EventHandler(ProductsGrid_Load);
- ((System.ComponentModel.ISupportInitialize)(gridEntryDataGridView)).EndInit();
- ((System.ComponentModel.ISupportInitialize)(syncBindingSource)).EndInit();
- ResumeLayout(false);
-
- }
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle3 = new System.Windows.Forms.DataGridViewCellStyle();
+ System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle4 = new System.Windows.Forms.DataGridViewCellStyle();
+ System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
+ System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
+ gridEntryDataGridView = new System.Windows.Forms.DataGridView();
+ removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
+ liberateGVColumn = new LiberateDataGridViewImageButtonColumn();
+ coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
+ playlistColumn = new System.Windows.Forms.DataGridViewButtonColumn();
+ titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ authorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ narratorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ lengthGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ seriesGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ seriesOrderGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ descriptionGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ categoryGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ productRatingGVColumn = new MyRatingGridViewColumn();
+ purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ myRatingGVColumn = new MyRatingGridViewColumn();
+ miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ lastDownloadedGVColumn = new LastDownloadedGridViewColumn();
+ tagAndDetailsGVColumn = new EditTagsDataGridViewImageButtonColumn();
+ showHideColumnsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
+ syncBindingSource = new SyncBindingSource(components);
+ ((System.ComponentModel.ISupportInitialize)gridEntryDataGridView).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)syncBindingSource).BeginInit();
+ SuspendLayout();
+ //
+ // gridEntryDataGridView
+ //
+ gridEntryDataGridView.AllowUserToAddRows = false;
+ gridEntryDataGridView.AllowUserToDeleteRows = false;
+ gridEntryDataGridView.AllowUserToOrderColumns = true;
+ gridEntryDataGridView.AllowUserToResizeRows = false;
+ gridEntryDataGridView.AutoGenerateColumns = false;
+ gridEntryDataGridView.BackgroundColor = System.Drawing.SystemColors.Window;
+ gridEntryDataGridView.BorderStyle = System.Windows.Forms.BorderStyle.None;
+ gridEntryDataGridView.CellBorderStyle = System.Windows.Forms.DataGridViewCellBorderStyle.None;
+ gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
+ gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { removeGVColumn, liberateGVColumn, coverGVColumn, playlistColumn, titleGVColumn, authorsGVColumn, narratorsGVColumn, lengthGVColumn, seriesGVColumn, seriesOrderGVColumn, descriptionGVColumn, categoryGVColumn, productRatingGVColumn, purchaseDateGVColumn, myRatingGVColumn, miscGVColumn, lastDownloadedGVColumn, tagAndDetailsGVColumn });
+ gridEntryDataGridView.ContextMenuStrip = showHideColumnsContextMenuStrip;
+ gridEntryDataGridView.DataSource = syncBindingSource;
+ dataGridViewCellStyle3.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
+ dataGridViewCellStyle3.BackColor = System.Drawing.SystemColors.Window;
+ dataGridViewCellStyle3.Font = new System.Drawing.Font("Segoe UI", 9F);
+ dataGridViewCellStyle3.ForeColor = System.Drawing.SystemColors.ControlText;
+ dataGridViewCellStyle3.SelectionBackColor = System.Drawing.SystemColors.Highlight;
+ dataGridViewCellStyle3.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
+ dataGridViewCellStyle3.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
+ gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle3;
+ gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
+ gridEntryDataGridView.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter;
+ gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
+ gridEntryDataGridView.Margin = new System.Windows.Forms.Padding(10);
+ gridEntryDataGridView.MultiSelect = false;
+ gridEntryDataGridView.Name = "gridEntryDataGridView";
+ gridEntryDataGridView.RowHeadersBorderStyle = System.Windows.Forms.DataGridViewHeaderBorderStyle.None;
+ gridEntryDataGridView.RowHeadersVisible = false;
+ gridEntryDataGridView.RowHeadersWidth = 82;
+ dataGridViewCellStyle4.BackColor = System.Drawing.SystemColors.Control;
+ gridEntryDataGridView.RowsDefaultCellStyle = dataGridViewCellStyle4;
+ gridEntryDataGridView.RowTemplate.Height = 82;
+ gridEntryDataGridView.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
+ gridEntryDataGridView.Size = new System.Drawing.Size(2748, 665);
+ gridEntryDataGridView.TabIndex = 0;
+ gridEntryDataGridView.CellContentClick += DataGridView_CellContentClick;
+ gridEntryDataGridView.CellToolTipTextNeeded += gridEntryDataGridView_CellToolTipTextNeeded;
+ gridEntryDataGridView.RowsAdded += gridEntryDataGridView_RowsAdded;
+ //
+ // removeGVColumn
+ //
+ removeGVColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.DisplayedCells;
+ removeGVColumn.DataPropertyName = "Remove";
+ removeGVColumn.FalseValue = "";
+ removeGVColumn.Frozen = true;
+ removeGVColumn.HeaderText = "Remove";
+ removeGVColumn.IndeterminateValue = "";
+ removeGVColumn.MinimumWidth = 60;
+ removeGVColumn.Name = "removeGVColumn";
+ removeGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
+ removeGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
+ removeGVColumn.ThreeState = true;
+ removeGVColumn.TrueValue = "";
+ removeGVColumn.Width = 187;
+ //
+ // liberateGVColumn
+ //
+ liberateGVColumn.DataPropertyName = "Liberate";
+ liberateGVColumn.HeaderText = "Liberate";
+ liberateGVColumn.MinimumWidth = 10;
+ liberateGVColumn.Name = "liberateGVColumn";
+ liberateGVColumn.ReadOnly = true;
+ liberateGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
+ liberateGVColumn.ScaleFactor = 0F;
+ liberateGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
+ liberateGVColumn.Width = 75;
+ //
+ // coverGVColumn
+ //
+ coverGVColumn.DataPropertyName = "Cover";
+ coverGVColumn.HeaderText = "Cover";
+ coverGVColumn.ImageLayout = System.Windows.Forms.DataGridViewImageCellLayout.Zoom;
+ coverGVColumn.MinimumWidth = 10;
+ coverGVColumn.Name = "coverGVColumn";
+ coverGVColumn.ReadOnly = true;
+ coverGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
+ coverGVColumn.ToolTipText = "Cover Art";
+ coverGVColumn.Width = 80;
+ //
+ // playlistColumn
+ //
+ playlistColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.DisplayedCells;
+ playlistColumn.DataPropertyName = "AddToPlaylistText";
+ dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
+ dataGridViewCellStyle1.Padding = new System.Windows.Forms.Padding(8);
+ dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.False;
+ playlistColumn.DefaultCellStyle = dataGridViewCellStyle1;
+ playlistColumn.HeaderText = "Playlist";
+ playlistColumn.MinimumWidth = 9;
+ playlistColumn.Name = "playlistColumn";
+ playlistColumn.ReadOnly = true;
+ playlistColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
+ playlistColumn.Text = "??????";
+ playlistColumn.Width = 133;
+ //
+ // titleGVColumn
+ //
+ titleGVColumn.DataPropertyName = "Title";
+ titleGVColumn.HeaderText = "Title";
+ titleGVColumn.MinimumWidth = 10;
+ titleGVColumn.Name = "titleGVColumn";
+ titleGVColumn.ReadOnly = true;
+ titleGVColumn.Width = 200;
+ //
+ // authorsGVColumn
+ //
+ authorsGVColumn.DataPropertyName = "Authors";
+ authorsGVColumn.HeaderText = "Authors";
+ authorsGVColumn.MinimumWidth = 10;
+ authorsGVColumn.Name = "authorsGVColumn";
+ authorsGVColumn.ReadOnly = true;
+ authorsGVColumn.Width = 175;
+ //
+ // narratorsGVColumn
+ //
+ narratorsGVColumn.DataPropertyName = "Narrators";
+ narratorsGVColumn.HeaderText = "Narrators";
+ narratorsGVColumn.MinimumWidth = 10;
+ narratorsGVColumn.Name = "narratorsGVColumn";
+ narratorsGVColumn.ReadOnly = true;
+ narratorsGVColumn.Width = 175;
+ //
+ // lengthGVColumn
+ //
+ lengthGVColumn.DataPropertyName = "Length";
+ lengthGVColumn.HeaderText = "Length";
+ lengthGVColumn.MinimumWidth = 10;
+ lengthGVColumn.Name = "lengthGVColumn";
+ lengthGVColumn.ReadOnly = true;
+ lengthGVColumn.ToolTipText = "Recording Length";
+ lengthGVColumn.Width = 175;
+ //
+ // seriesGVColumn
+ //
+ seriesGVColumn.DataPropertyName = "Series";
+ seriesGVColumn.HeaderText = "Series";
+ seriesGVColumn.MinimumWidth = 10;
+ seriesGVColumn.Name = "seriesGVColumn";
+ seriesGVColumn.ReadOnly = true;
+ seriesGVColumn.Width = 175;
+ //
+ // seriesOrderGVColumn
+ //
+ seriesOrderGVColumn.DataPropertyName = "SeriesOrder";
+ dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter;
+ seriesOrderGVColumn.DefaultCellStyle = dataGridViewCellStyle2;
+ seriesOrderGVColumn.HeaderText = "Series\r\nOrder";
+ seriesOrderGVColumn.MinimumWidth = 10;
+ seriesOrderGVColumn.Name = "seriesOrderGVColumn";
+ seriesOrderGVColumn.ReadOnly = true;
+ seriesOrderGVColumn.Width = 60;
+ //
+ // descriptionGVColumn
+ //
+ descriptionGVColumn.DataPropertyName = "Description";
+ descriptionGVColumn.HeaderText = "Description";
+ descriptionGVColumn.MinimumWidth = 10;
+ descriptionGVColumn.Name = "descriptionGVColumn";
+ descriptionGVColumn.ReadOnly = true;
+ descriptionGVColumn.Width = 175;
+ //
+ // categoryGVColumn
+ //
+ categoryGVColumn.DataPropertyName = "Category";
+ categoryGVColumn.HeaderText = "Category";
+ categoryGVColumn.MinimumWidth = 10;
+ categoryGVColumn.Name = "categoryGVColumn";
+ categoryGVColumn.ReadOnly = true;
+ categoryGVColumn.Width = 175;
+ //
+ // productRatingGVColumn
+ //
+ productRatingGVColumn.DataPropertyName = "ProductRating";
+ productRatingGVColumn.HeaderText = "Product Rating";
+ productRatingGVColumn.MinimumWidth = 10;
+ productRatingGVColumn.Name = "productRatingGVColumn";
+ productRatingGVColumn.ReadOnly = true;
+ productRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
+ productRatingGVColumn.Width = 112;
+ //
+ // purchaseDateGVColumn
+ //
+ purchaseDateGVColumn.DataPropertyName = "PurchaseDate";
+ purchaseDateGVColumn.HeaderText = "Purchase Date";
+ purchaseDateGVColumn.MinimumWidth = 10;
+ purchaseDateGVColumn.Name = "purchaseDateGVColumn";
+ purchaseDateGVColumn.ReadOnly = true;
+ purchaseDateGVColumn.Width = 175;
+ //
+ // myRatingGVColumn
+ //
+ myRatingGVColumn.DataPropertyName = "MyRating";
+ myRatingGVColumn.HeaderText = "My Rating";
+ myRatingGVColumn.MinimumWidth = 10;
+ myRatingGVColumn.Name = "myRatingGVColumn";
+ myRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
+ myRatingGVColumn.Width = 112;
+ //
+ // miscGVColumn
+ //
+ miscGVColumn.DataPropertyName = "Misc";
+ miscGVColumn.HeaderText = "Misc";
+ miscGVColumn.MinimumWidth = 10;
+ miscGVColumn.Name = "miscGVColumn";
+ miscGVColumn.ReadOnly = true;
+ miscGVColumn.Width = 140;
+ //
+ // lastDownloadedGVColumn
+ //
+ lastDownloadedGVColumn.DataPropertyName = "LastDownload";
+ lastDownloadedGVColumn.HeaderText = "Last Download";
+ lastDownloadedGVColumn.MinimumWidth = 10;
+ lastDownloadedGVColumn.Name = "lastDownloadedGVColumn";
+ lastDownloadedGVColumn.ReadOnly = true;
+ lastDownloadedGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
+ lastDownloadedGVColumn.Width = 108;
+ //
+ // tagAndDetailsGVColumn
+ //
+ tagAndDetailsGVColumn.DataPropertyName = "BookTags";
+ tagAndDetailsGVColumn.HeaderText = "Tags and Details";
+ tagAndDetailsGVColumn.MinimumWidth = 10;
+ tagAndDetailsGVColumn.Name = "tagAndDetailsGVColumn";
+ tagAndDetailsGVColumn.ReadOnly = true;
+ tagAndDetailsGVColumn.ScaleFactor = 0F;
+ tagAndDetailsGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
+ tagAndDetailsGVColumn.Width = 175;
+ //
+ // showHideColumnsContextMenuStrip
+ //
+ showHideColumnsContextMenuStrip.ImageScalingSize = new System.Drawing.Size(32, 32);
+ showHideColumnsContextMenuStrip.Name = "contextMenuStrip1";
+ showHideColumnsContextMenuStrip.ShowCheckMargin = true;
+ showHideColumnsContextMenuStrip.Size = new System.Drawing.Size(83, 4);
+ //
+ // syncBindingSource
+ //
+ syncBindingSource.DataSource = typeof(IGridEntry);
+ //
+ // ProductsGrid
+ //
+ AutoScaleDimensions = new System.Drawing.SizeF(168F, 168F);
+ AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
+ AutoScroll = true;
+ Controls.Add(gridEntryDataGridView);
+ Margin = new System.Windows.Forms.Padding(5);
+ Name = "ProductsGrid";
+ Size = new System.Drawing.Size(2748, 665);
+ Load += ProductsGrid_Load;
+ ((System.ComponentModel.ISupportInitialize)gridEntryDataGridView).EndInit();
+ ((System.ComponentModel.ISupportInitialize)syncBindingSource).EndInit();
+ ResumeLayout(false);
+ }
- #endregion
- private System.Windows.Forms.DataGridView gridEntryDataGridView;
+ #endregion
+ private System.Windows.Forms.DataGridView gridEntryDataGridView;
private System.Windows.Forms.ContextMenuStrip showHideColumnsContextMenuStrip;
private SyncBindingSource syncBindingSource;
- private System.Windows.Forms.DataGridViewCheckBoxColumn removeGVColumn;
- private LiberateDataGridViewImageButtonColumn liberateGVColumn;
- private System.Windows.Forms.DataGridViewImageColumn coverGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn titleGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn authorsGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn narratorsGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn lengthGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn seriesGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn seriesOrderGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn descriptionGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn categoryGVColumn;
- private MyRatingGridViewColumn productRatingGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn purchaseDateGVColumn;
- private MyRatingGridViewColumn myRatingGVColumn;
- private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;
- private LastDownloadedGridViewColumn lastDownloadedGVColumn;
- private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn;
- }
+ private System.Windows.Forms.DataGridViewCheckBoxColumn removeGVColumn;
+ private LiberateDataGridViewImageButtonColumn liberateGVColumn;
+ private System.Windows.Forms.DataGridViewImageColumn coverGVColumn;
+ private System.Windows.Forms.DataGridViewButtonColumn playlistColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn titleGVColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn authorsGVColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn narratorsGVColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn lengthGVColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn seriesGVColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn seriesOrderGVColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn descriptionGVColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn categoryGVColumn;
+ private MyRatingGridViewColumn productRatingGVColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn purchaseDateGVColumn;
+ private MyRatingGridViewColumn myRatingGVColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;
+ private LastDownloadedGridViewColumn lastDownloadedGVColumn;
+ private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn;
+ }
}
diff --git a/Source/LibationWinForms/GridView/ProductsGrid.cs b/Source/LibationWinForms/GridView/ProductsGrid.cs
index 790f5ba03..37254fe23 100644
--- a/Source/LibationWinForms/GridView/ProductsGrid.cs
+++ b/Source/LibationWinForms/GridView/ProductsGrid.cs
@@ -2,526 +2,628 @@
using Dinah.Core;
using Dinah.Core.WindowsDesktop.Forms;
using LibationFileManager;
+using LibationUiBase;
using LibationUiBase.GridView;
-using NPOI.SS.Formula.Functions;
+using LibationUiBase.ViewModels.Player;
+using Prism.Events;
using System;
using System.Collections.Generic;
-using System.Data;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
+using Color = System.Drawing.Color;
namespace LibationWinForms.GridView
{
- public delegate void GridEntryClickedEventHandler(IGridEntry liveGridEntry);
- public delegate void LibraryBookEntryClickedEventHandler(ILibraryBookEntry liveGridEntry);
- public delegate void GridEntryRectangleClickedEventHandler(IGridEntry liveGridEntry, Rectangle cellRectangle);
- public delegate void ProductsGridCellContextMenuStripNeededEventHandler(IGridEntry liveGridEntry, ContextMenuStrip ctxMenu);
-
- public partial class ProductsGrid : UserControl
- {
- /// Number of visible rows has changed
- public event EventHandler VisibleCountChanged;
- public event LibraryBookEntryClickedEventHandler LiberateClicked;
- public event GridEntryClickedEventHandler CoverClicked;
- public event LibraryBookEntryClickedEventHandler DetailsClicked;
- public event GridEntryRectangleClickedEventHandler DescriptionClicked;
- public new event EventHandler Scroll;
- public event EventHandler RemovableCountChanged;
- public event ProductsGridCellContextMenuStripNeededEventHandler LiberateContextMenuStripNeeded;
-
- private GridEntryBindingList bindingList;
- internal IEnumerable GetVisibleBooks()
- => bindingList
- .GetFilteredInItems()
- .Select(lbe => lbe.LibraryBook);
- internal IEnumerable GetAllBookEntries()
- => bindingList.AllItems().BookEntries();
-
- public ProductsGrid()
- {
- InitializeComponent();
- EnableDoubleBuffering();
- gridEntryDataGridView.Scroll += (_, s) => Scroll?.Invoke(this, s);
- gridEntryDataGridView.CellContextMenuStripNeeded += GridEntryDataGridView_CellContextMenuStripNeeded;
- removeGVColumn.Frozen = false;
-
- defaultFont = gridEntryDataGridView.DefaultCellStyle.Font;
- setGridFontScale(Configuration.Instance.GridFontScaleFactor);
- setGridScale(Configuration.Instance.GridScaleFactor);
- Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;
- Configuration.Instance.PropertyChanged += Configuration_FontScaleChanged;
-
- gridEntryDataGridView.Disposed += (_, _) =>
- {
- Configuration.Instance.PropertyChanged -= Configuration_ScaleChanged;
- Configuration.Instance.PropertyChanged -= Configuration_FontScaleChanged;
- };
- }
-
- #region Scaling
-
- [PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
- private void Configuration_FontScaleChanged(object sender, PropertyChangedEventArgsEx e)
- => setGridFontScale((float)e.NewValue);
-
- [PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
- private void Configuration_ScaleChanged(object sender, PropertyChangedEventArgsEx e)
- => setGridScale((float)e.NewValue);
-
- ///
- /// Keep track of the original dimensions for rescaling
- ///
- private static readonly Dictionary originalDims = new();
- private readonly Font defaultFont;
- private void setGridScale(float scale)
- {
- foreach (var col in gridEntryDataGridView.Columns.Cast())
- {
- //Only resize fixed-width columns. The rest can be adjusted by users.
- if (col.Resizable is DataGridViewTriState.False)
- {
- if (!originalDims.ContainsKey(col))
- originalDims[col] = col.Width;
-
- col.Width = this.DpiScale(originalDims[col], scale);
- }
-
- if (col is IDataGridScaleColumn scCol)
- scCol.ScaleFactor = scale;
- }
-
- if (!originalDims.ContainsKey(gridEntryDataGridView.RowTemplate))
- originalDims[gridEntryDataGridView.RowTemplate] = gridEntryDataGridView.RowTemplate.Height;
-
- var height = gridEntryDataGridView.RowTemplate.Height = this.DpiScale(originalDims[gridEntryDataGridView.RowTemplate], scale);
-
- foreach (var row in gridEntryDataGridView.Rows.Cast())
- row.Height = height;
- }
-
- private void setGridFontScale(float scale)
- => gridEntryDataGridView.DefaultCellStyle.Font = new Font(defaultFont.FontFamily, defaultFont.Size * scale);
-
- #endregion
-
- private void GridEntryDataGridView_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
- {
- // header
- if (e.RowIndex < 0)
- return;
-
- e.ContextMenuStrip = new ContextMenuStrip();
- // any column except cover & stop light
- if (e.ColumnIndex != liberateGVColumn.Index && e.ColumnIndex != coverGVColumn.Index)
- {
- e.ContextMenuStrip.Items.Add("Copy Cell Contents", null, (_, __) =>
- {
- try
- {
- var dgv = (DataGridView)sender;
- var text = dgv[e.ColumnIndex, e.RowIndex].FormattedValue.ToString();
- Clipboard.SetDataObject(text, false, 5, 150);
- }
- catch { }
- });
- e.ContextMenuStrip.Items.Add(new ToolStripSeparator());
- }
-
- var entry = getGridEntry(e.RowIndex);
- var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
- LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip);
- }
-
- private void EnableDoubleBuffering()
- {
- var propertyInfo = gridEntryDataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
-
- propertyInfo.SetValue(gridEntryDataGridView, true, null);
- }
-
- #region Button controls
- private void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
- {
- try
- {
- // handle grid button click: https://stackoverflow.com/a/13687844
- if (e.RowIndex < 0)
- return;
-
- var entry = getGridEntry(e.RowIndex);
- if (entry is ILibraryBookEntry lbEntry)
- {
- if (e.ColumnIndex == liberateGVColumn.Index)
- LiberateClicked?.Invoke(lbEntry);
- else if (e.ColumnIndex == tagAndDetailsGVColumn.Index)
- DetailsClicked?.Invoke(lbEntry);
- else if (e.ColumnIndex == descriptionGVColumn.Index)
- DescriptionClicked?.Invoke(lbEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
- else if (e.ColumnIndex == coverGVColumn.Index)
- CoverClicked?.Invoke(lbEntry);
- }
- else if (entry is ISeriesEntry sEntry)
- {
- if (e.ColumnIndex == liberateGVColumn.Index)
- {
- if (sEntry.Liberate.Expanded)
- bindingList.CollapseItem(sEntry);
- else
- bindingList.ExpandItem(sEntry);
-
- VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
- }
- else if (e.ColumnIndex == descriptionGVColumn.Index)
- DescriptionClicked?.Invoke(sEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
- else if (e.ColumnIndex == coverGVColumn.Index)
- CoverClicked?.Invoke(sEntry);
- }
-
- if (e.ColumnIndex == removeGVColumn.Index)
- {
- gridEntryDataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
- RemovableCountChanged?.Invoke(this, EventArgs.Empty);
- }
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, $"An error was encountered while processing a user click in the {nameof(ProductsGrid)}");
- }
- }
-
- private IGridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem(rowIndex);
-
- #endregion
-
- #region UI display functions
-
- internal bool RemoveColumnVisible
- {
- get => removeGVColumn.Visible;
- set
- {
- if (value)
- {
- foreach (var book in bindingList.AllItems())
- book.Remove = false;
- }
-
- removeGVColumn.DisplayIndex = 0;
- removeGVColumn.Frozen = value;
- removeGVColumn.Visible = value;
- }
- }
-
- internal async Task BindToGridAsync(List dbBooks)
- {
- var geList = await LibraryBookEntry.GetAllProductsAsync(dbBooks);
-
- var seriesEntries = await SeriesEntry.GetAllSeriesEntriesAsync(dbBooks);
-
- geList.AddRange(seriesEntries);
- //Sort descending by date (default sort property)
- var comparer = new RowComparer();
- geList.Sort((a, b) => comparer.Compare(b, a));
-
- //Add all children beneath their parent
- foreach (var series in seriesEntries)
- {
- var seriesIndex = geList.IndexOf(series);
- foreach (var child in series.Children)
- geList.Insert(++seriesIndex, child);
- }
-
- bindingList = new GridEntryBindingList(geList);
- bindingList.CollapseAll();
- syncBindingSource.DataSource = bindingList;
- VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
- }
-
- internal void UpdateGrid(List dbBooks)
- {
- //First row that is in view in the DataGridView
- var topRow = gridEntryDataGridView.Rows.Cast().FirstOrDefault(r => r.Displayed)?.Index ?? 0;
-
- #region Add new or update existing grid entries
-
- //Remove filter prior to adding/updating boooks
- string existingFilter = syncBindingSource.Filter;
- Filter(null);
-
- //Add absent entries to grid, or update existing entry
-
- var allEntries = bindingList.AllItems().BookEntries();
- var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
- var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
-
- bindingList.RaiseListChangedEvents = false;
- foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
- {
- var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
-
- if (libraryBook.Book.IsProduct())
- {
- AddOrUpdateBook(libraryBook, existingEntry);
- continue;
- }
- if (parentedEpisodes.Contains(libraryBook))
- {
- //Only try to add or update is this LibraryBook is a know child of a parent
- AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
- }
- }
- bindingList.RaiseListChangedEvents = true;
-
- //Re-apply filter after adding new/updating existing books to capture any changes
- //The Filter call also ensures that the binding list is reset so the DataGridView
- //is made aware of all changes that were made while RaiseListChangedEvents was false
- Filter(existingFilter);
-
- #endregion
-
- // remove deleted from grid.
- // note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
- var removedBooks =
- bindingList
- .AllItems()
- .BookEntries()
- .ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
-
- RemoveBooks(removedBooks);
-
- gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
- }
-
- public void RemoveBooks(IEnumerable removedBooks)
- {
- //Remove books in series from their parents' Children list
- foreach (var removed in removedBooks.Where(b => b.Liberate.IsEpisode))
- removed.Parent.RemoveChild(removed);
-
- //Remove series that have no children
- var removedSeries =
- bindingList
- .AllItems()
- .EmptySeries();
-
- foreach (var removed in removedBooks.Cast().Concat(removedSeries))
- //no need to re-filter for removed books
- bindingList.Remove(removed);
-
- VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
- }
-
- private void AddOrUpdateBook(LibraryBook book, ILibraryBookEntry existingBookEntry)
- {
- if (existingBookEntry is null)
- // Add the new product to top
- bindingList.Insert(0, new LibraryBookEntry(book));
- else
- // update existing
- existingBookEntry.UpdateLibraryBook(book);
- }
-
- private void AddOrUpdateEpisode(LibraryBook episodeBook, ILibraryBookEntry existingEpisodeEntry, List seriesEntries, IEnumerable dbBooks)
- {
- if (existingEpisodeEntry is null)
- {
- ILibraryBookEntry episodeEntry;
-
- var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
-
- if (seriesEntry is null)
- {
- //Series doesn't exist yet, so create and add it
- var seriesBook = dbBooks.FindSeriesParent(episodeBook);
-
- if (seriesBook is null)
- {
- //This is only possible if the user's db has some malformed
- //entries from earlier Libation releases that could not be
- //automatically fixed. Log, but don't throw.
- Serilog.Log.Logger.Error("Episode={0}, Episode Series: {1}", episodeBook, episodeBook.Book.SeriesNames());
- return;
- }
-
- seriesEntry = new SeriesEntry(seriesBook, episodeBook);
- seriesEntries.Add(seriesEntry);
-
- episodeEntry = seriesEntry.Children[0];
- seriesEntry.Liberate.Expanded = true;
- bindingList.Insert(0, seriesEntry);
- }
- else
- {
- //Series exists. Create and add episode child then update the SeriesEntry
- episodeEntry = new LibraryBookEntry(episodeBook, seriesEntry);
- seriesEntry.Children.Add(episodeEntry);
- seriesEntry.Children.Sort((c1, c2) => c1.SeriesIndex.CompareTo(c2.SeriesIndex));
- var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
- seriesEntry.UpdateLibraryBook(seriesBook);
- }
-
- //Series entry must be expanded so its child can
- //be placed in the correct position beneath it.
- var isExpanded = seriesEntry.Liberate.Expanded;
- bindingList.ExpandItem(seriesEntry);
-
- //Add episode to the grid beneath the parent
- int seriesIndex = bindingList.IndexOf(seriesEntry);
- int episodeIndex = seriesEntry.Children.IndexOf(episodeEntry);
- bindingList.Insert(seriesIndex + 1 + episodeIndex, episodeEntry);
-
- if (isExpanded)
- bindingList.ExpandItem(seriesEntry);
- else
- bindingList.CollapseItem(seriesEntry);
- }
- else
- existingEpisodeEntry.UpdateLibraryBook(episodeBook);
- }
-
- #endregion
-
- #region Filter
-
- public void Filter(string searchString)
- {
- if (bindingList is null) return;
-
- int visibleCount = bindingList.Count;
-
- if (string.IsNullOrEmpty(searchString))
- syncBindingSource.RemoveFilter();
- else
- syncBindingSource.Filter = searchString;
-
- if (visibleCount != bindingList.Count)
- VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
- }
-
- #endregion
-
- #region Column Customizations
-
- private void ProductsGrid_Load(object sender, EventArgs e)
- {
- //https://stackoverflow.com/a/4498512/3335599
- if (System.ComponentModel.LicenseManager.UsageMode == System.ComponentModel.LicenseUsageMode.Designtime) return;
-
- gridEntryDataGridView.ColumnWidthChanged += gridEntryDataGridView_ColumnWidthChanged;
- gridEntryDataGridView.ColumnDisplayIndexChanged += gridEntryDataGridView_ColumnDisplayIndexChanged;
-
- showHideColumnsContextMenuStrip.Items.Add(new ToolStripLabel("Show / Hide Columns"));
- showHideColumnsContextMenuStrip.Items.Add(new ToolStripSeparator());
-
- //Restore Grid Display Settings
- var config = Configuration.Instance;
- var gridColumnsVisibilities = config.GridColumnsVisibilities;
- var gridColumnsWidths = config.GridColumnsWidths;
- var displayIndices = config.GridColumnsDisplayIndices;
-
- var cmsKiller = new ContextMenuStrip();
-
- foreach (DataGridViewColumn column in gridEntryDataGridView.Columns)
- {
- var itemName = column.DataPropertyName;
- var visible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
-
- var menuItem = new ToolStripMenuItem(column.HeaderText)
- {
- Checked = visible,
- Tag = itemName
- };
- menuItem.Click += HideMenuItem_Click;
- showHideColumnsContextMenuStrip.Items.Add(menuItem);
-
- //Only set column widths for user resizable columns.
- //Fixed column widths are set by setGridScale()
- if (column.Resizable is not DataGridViewTriState.False)
- column.Width = gridColumnsWidths.GetValueOrDefault(itemName, this.DpiScale(column.Width));
-
- column.MinimumWidth = 10;
- column.HeaderCell.ContextMenuStrip = showHideColumnsContextMenuStrip;
- column.Visible = visible;
-
- //Setting a default ContextMenuStrip will allow the columns to handle the
- //Show() event so it is not passed up to the _dataGridView.ContextMenuStrip.
- //This allows the ContextMenuStrip to be shown if right-clicking in the gray
- //background of _dataGridView but not shown if right-clicking inside cells.
- column.ContextMenuStrip = cmsKiller;
- }
-
- //We must set DisplayIndex properties in ascending order
- foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
- {
- var column = gridEntryDataGridView.Columns
- .Cast()
- .SingleOrDefault(c => c.DataPropertyName == itemName);
-
- if (column is null) continue;
-
- column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index);
- }
-
- //Remove column is always first;
- removeGVColumn.DisplayIndex = 0;
- removeGVColumn.Visible = false;
- removeGVColumn.ValueType = typeof(bool?);
- removeGVColumn.FalseValue = false;
- removeGVColumn.TrueValue = true;
- removeGVColumn.IndeterminateValue = null;
- }
-
- private void HideMenuItem_Click(object sender, EventArgs e)
- {
- var menuItem = sender as ToolStripMenuItem;
- var propertyName = menuItem.Tag as string;
-
- var column = gridEntryDataGridView.Columns
- .Cast()
- .FirstOrDefault(c => c.DataPropertyName == propertyName);
-
- if (column != null)
- {
- var visible = menuItem.Checked;
- menuItem.Checked = !visible;
- column.Visible = !visible;
-
- var config = Configuration.Instance;
-
- var dictionary = config.GridColumnsVisibilities;
- dictionary[propertyName] = column.Visible;
- config.GridColumnsVisibilities = dictionary;
- }
- }
-
- private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
- {
- var config = Configuration.Instance;
-
- var dictionary = config.GridColumnsDisplayIndices;
- dictionary[e.Column.DataPropertyName] = e.Column.DisplayIndex;
- config.GridColumnsDisplayIndices = dictionary;
- }
-
- private void gridEntryDataGridView_CellToolTipTextNeeded(object sender, DataGridViewCellToolTipTextNeededEventArgs e)
- {
- if (e.ColumnIndex == descriptionGVColumn.Index)
- e.ToolTipText = "Click to see full description";
- else if (e.ColumnIndex == coverGVColumn.Index)
- e.ToolTipText = "Click to see full size";
- }
-
- private void gridEntryDataGridView_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
- {
- var config = Configuration.Instance;
-
- var dictionary = config.GridColumnsWidths;
- dictionary[e.Column.DataPropertyName] = e.Column.Width;
- config.GridColumnsWidths = dictionary;
- }
-
- #endregion
- }
+ public delegate void GridEntryClickedEventHandler(IGridEntry liveGridEntry);
+ public delegate void LibraryBookEntryClickedEventHandler(ILibraryBookEntry liveGridEntry);
+ public delegate void GridEntryRectangleClickedEventHandler(IGridEntry liveGridEntry, Rectangle cellRectangle);
+ public delegate void ProductsGridCellContextMenuStripNeededEventHandler(IGridEntry liveGridEntry, ContextMenuStrip ctxMenu);
+
+ public partial class ProductsGrid : UserControl
+ {
+ //IEventAggregator eventAggregator;
+ PlayerViewModel _player = ServiceLocator.Get();
+
+ /// Number of visible rows has changed
+ public event EventHandler VisibleCountChanged;
+ public event LibraryBookEntryClickedEventHandler LiberateClicked;
+ public event GridEntryClickedEventHandler CoverClicked;
+ public event LibraryBookEntryClickedEventHandler DetailsClicked;
+ public event GridEntryRectangleClickedEventHandler DescriptionClicked;
+ public new event EventHandler Scroll;
+ public event EventHandler RemovableCountChanged;
+ public event ProductsGridCellContextMenuStripNeededEventHandler LiberateContextMenuStripNeeded;
+
+ private GridEntryBindingList bindingList;
+ internal IEnumerable GetVisibleBooks()
+ {
+ return bindingList
+ .GetFilteredInItems()
+ .Select(lbe => lbe.LibraryBook);
+ }
+
+ internal IEnumerable GetAllBookEntries()
+ => bindingList.AllItems().BookEntries();
+
+ public ProductsGrid()
+ {
+ InitializeComponent();
+ EnableDoubleBuffering();
+ gridEntryDataGridView.Scroll += (_, s) => Scroll?.Invoke(this, s);
+ gridEntryDataGridView.CellContextMenuStripNeeded += GridEntryDataGridView_CellContextMenuStripNeeded;
+ gridEntryDataGridView.CellClick += gridEntryDataGridView_CellClick;
+ removeGVColumn.Frozen = false;
+
+ // Hide the column selection when you're clicking in cells.
+ gridEntryDataGridView.ColumnHeadersDefaultCellStyle.SelectionBackColor =
+ gridEntryDataGridView.ColumnHeadersDefaultCellStyle.BackColor;
+ gridEntryDataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
+
+ defaultFont = gridEntryDataGridView.DefaultCellStyle.Font;
+ setGridFontScale(Configuration.Instance.GridFontScaleFactor);
+ setGridScale(Configuration.Instance.GridScaleFactor);
+ Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;
+ Configuration.Instance.PropertyChanged += Configuration_FontScaleChanged;
+
+ gridEntryDataGridView.Disposed += (_, _) =>
+ {
+ Configuration.Instance.PropertyChanged -= Configuration_ScaleChanged;
+ Configuration.Instance.PropertyChanged -= Configuration_FontScaleChanged;
+ };
+
+ //eventAggregator = ServiceLocator.Get();
+ //eventAggregator.GetEvent().Subscribe(InvalidateBookEntry, ThreadOption.PublisherThread);
+ //eventAggregator.GetEvent().Subscribe(InvalidateBookEntry, ThreadOption.PublisherThread);
+ }
+
+ #region Scaling
+
+ [PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
+ private void Configuration_FontScaleChanged(object sender, PropertyChangedEventArgsEx e)
+ => setGridFontScale((float)e.NewValue);
+
+ [PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
+ private void Configuration_ScaleChanged(object sender, PropertyChangedEventArgsEx e)
+ => setGridScale((float)e.NewValue);
+
+ ///
+ /// Keep track of the original dimensions for rescaling
+ ///
+ private static readonly Dictionary originalDims = new();
+ private readonly Font defaultFont;
+ private void setGridScale(float scale)
+ {
+ foreach (var col in gridEntryDataGridView.Columns.Cast())
+ {
+ //Only resize fixed-width columns. The rest can be adjusted by users.
+ if (col.Resizable is DataGridViewTriState.False)
+ {
+ if (!originalDims.ContainsKey(col))
+ originalDims[col] = col.Width;
+
+ col.Width = this.DpiScale(originalDims[col], scale);
+ }
+
+ if (col is IDataGridScaleColumn scCol)
+ scCol.ScaleFactor = scale;
+ }
+
+ if (!originalDims.ContainsKey(gridEntryDataGridView.RowTemplate))
+ originalDims[gridEntryDataGridView.RowTemplate] = gridEntryDataGridView.RowTemplate.Height;
+
+ var height = gridEntryDataGridView.RowTemplate.Height = this.DpiScale(originalDims[gridEntryDataGridView.RowTemplate], scale);
+
+ foreach (var row in gridEntryDataGridView.Rows.Cast())
+ row.Height = height;
+ }
+
+ private void setGridFontScale(float scale)
+ => gridEntryDataGridView.DefaultCellStyle.Font = new Font(defaultFont.FontFamily, defaultFont.Size * scale);
+
+ #endregion
+
+ private void GridEntryDataGridView_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
+ {
+ // header
+ if (e.RowIndex < 0)
+ return;
+
+ e.ContextMenuStrip = new ContextMenuStrip();
+ // any column except cover & stop light
+ if (e.ColumnIndex != liberateGVColumn.Index && e.ColumnIndex != coverGVColumn.Index)
+ {
+ e.ContextMenuStrip.Items.Add("Copy Cell Contents", null, (_, __) =>
+ {
+ try
+ {
+ var dgv = (DataGridView)sender;
+ var text = dgv[e.ColumnIndex, e.RowIndex].FormattedValue.ToString();
+ Clipboard.SetDataObject(text, false, 5, 150);
+ }
+ catch { }
+ });
+ e.ContextMenuStrip.Items.Add(new ToolStripSeparator());
+ }
+
+ var entry = getGridEntry(e.RowIndex);
+ var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
+ LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip);
+ }
+
+ private void EnableDoubleBuffering()
+ {
+ var propertyInfo = gridEntryDataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
+
+ propertyInfo.SetValue(gridEntryDataGridView, true, null);
+ }
+
+ #region Button controls
+ private void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
+ {
+ try
+ {
+ // handle grid button click: https://stackoverflow.com/a/13687844
+ if (e.RowIndex < 0)
+ return;
+
+ var entry = getGridEntry(e.RowIndex);
+ if (entry is ILibraryBookEntry lbEntry)
+ {
+ if (e.ColumnIndex == liberateGVColumn.Index)
+ LiberateClicked?.Invoke(lbEntry);
+ else if (e.ColumnIndex == tagAndDetailsGVColumn.Index)
+ DetailsClicked?.Invoke(lbEntry);
+ else if (e.ColumnIndex == descriptionGVColumn.Index)
+ DescriptionClicked?.Invoke(lbEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
+ else if (e.ColumnIndex == coverGVColumn.Index)
+ CoverClicked?.Invoke(lbEntry);
+ }
+ else if (entry is ISeriesEntry sEntry)
+ {
+ if (e.ColumnIndex == liberateGVColumn.Index)
+ {
+ if (sEntry.Liberate.Expanded)
+ bindingList.CollapseItem(sEntry);
+ else
+ bindingList.ExpandItem(sEntry);
+
+ VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
+ }
+ else if (e.ColumnIndex == descriptionGVColumn.Index)
+ DescriptionClicked?.Invoke(sEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
+ else if (e.ColumnIndex == coverGVColumn.Index)
+ CoverClicked?.Invoke(sEntry);
+ }
+
+ if (e.ColumnIndex == removeGVColumn.Index)
+ {
+ gridEntryDataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
+ RemovableCountChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Logger.Error(ex, $"An error was encountered while processing a user click in the {nameof(ProductsGrid)}");
+ }
+ }
+
+ private IGridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem(rowIndex);
+
+ #endregion
+
+ #region UI display functions
+
+ internal bool RemoveColumnVisible
+ {
+ get => removeGVColumn.Visible;
+ set
+ {
+ if (value)
+ {
+ foreach (var book in bindingList.AllItems())
+ book.Remove = false;
+ }
+
+ removeGVColumn.DisplayIndex = 0;
+ removeGVColumn.Frozen = value;
+ removeGVColumn.Visible = value;
+ }
+ }
+
+ internal async Task BindToGridAsync(List dbBooks)
+ {
+ var geList = await LibraryBookEntry.GetAllProductsAsync(dbBooks);
+
+ var seriesEntries = await SeriesEntry.GetAllSeriesEntriesAsync(dbBooks);
+
+ geList.AddRange(seriesEntries);
+ //Sort descending by date (default sort property)
+ var comparer = new RowComparer();
+ geList.Sort((a, b) => comparer.Compare(b, a));
+
+ //Add all children beneath their parent
+ foreach (var series in seriesEntries)
+ {
+ var seriesIndex = geList.IndexOf(series);
+ foreach (var child in series.Children)
+ geList.Insert(++seriesIndex, child);
+ }
+
+ bindingList = new GridEntryBindingList(geList);
+ bindingList.CollapseAll();
+ syncBindingSource.DataSource = bindingList;
+ VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
+ }
+
+ internal void UpdateGrid(List dbBooks)
+ {
+ //First row that is in view in the DataGridView
+ var topRow = gridEntryDataGridView.Rows.Cast().FirstOrDefault(r => r.Displayed)?.Index ?? 0;
+
+ #region Add new or update existing grid entries
+
+ //Remove filter prior to adding/updating boooks
+ string existingFilter = syncBindingSource.Filter;
+ Filter(null);
+
+ //Add absent entries to grid, or update existing entry
+
+ var allEntries = bindingList.AllItems().BookEntries();
+ var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
+ var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
+
+ bindingList.RaiseListChangedEvents = false;
+ foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
+ {
+ var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
+
+ if (libraryBook.Book.IsProduct())
+ {
+ AddOrUpdateBook(libraryBook, existingEntry);
+ continue;
+ }
+ if (parentedEpisodes.Contains(libraryBook))
+ {
+ //Only try to add or update is this LibraryBook is a know child of a parent
+ AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
+ }
+ }
+ bindingList.RaiseListChangedEvents = true;
+
+ //Re-apply filter after adding new/updating existing books to capture any changes
+ //The Filter call also ensures that the binding list is reset so the DataGridView
+ //is made aware of all changes that were made while RaiseListChangedEvents was false
+ Filter(existingFilter);
+
+ #endregion
+
+ // remove deleted from grid.
+ // note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
+ var removedBooks =
+ bindingList
+ .AllItems()
+ .BookEntries()
+ .ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
+
+ RemoveBooks(removedBooks);
+
+ gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
+ }
+
+ public void RemoveBooks(IEnumerable removedBooks)
+ {
+ //Remove books in series from their parents' Children list
+ foreach (var removed in removedBooks.Where(b => b.Liberate.IsEpisode))
+ removed.Parent.RemoveChild(removed);
+
+ //Remove series that have no children
+ var removedSeries =
+ bindingList
+ .AllItems()
+ .EmptySeries();
+
+ foreach (var removed in removedBooks.Cast().Concat(removedSeries))
+ //no need to re-filter for removed books
+ bindingList.Remove(removed);
+
+ VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
+ }
+
+ private void AddOrUpdateBook(LibraryBook book, ILibraryBookEntry existingBookEntry)
+ {
+ if (existingBookEntry is null)
+ // Add the new product to top
+ bindingList.Insert(0, new LibraryBookEntry(book));
+ else
+ // update existing
+ existingBookEntry.UpdateLibraryBook(book);
+ }
+
+ private void AddOrUpdateEpisode(LibraryBook episodeBook, ILibraryBookEntry existingEpisodeEntry, List seriesEntries, IEnumerable dbBooks)
+ {
+ if (existingEpisodeEntry is null)
+ {
+ ILibraryBookEntry episodeEntry;
+
+ var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
+
+ if (seriesEntry is null)
+ {
+ //Series doesn't exist yet, so create and add it
+ var seriesBook = dbBooks.FindSeriesParent(episodeBook);
+
+ if (seriesBook is null)
+ {
+ //This is only possible if the user's db has some malformed
+ //entries from earlier Libation releases that could not be
+ //automatically fixed. Log, but don't throw.
+ Serilog.Log.Logger.Error("Episode={0}, Episode Series: {1}", episodeBook, episodeBook.Book.SeriesNames());
+ return;
+ }
+
+ seriesEntry = new SeriesEntry(seriesBook, episodeBook);
+ seriesEntries.Add(seriesEntry);
+
+ episodeEntry = seriesEntry.Children[0];
+ seriesEntry.Liberate.Expanded = true;
+ bindingList.Insert(0, seriesEntry);
+ }
+ else
+ {
+ //Series exists. Create and add episode child then update the SeriesEntry
+ episodeEntry = new LibraryBookEntry(episodeBook, seriesEntry);
+ seriesEntry.Children.Add(episodeEntry);
+ seriesEntry.Children.Sort((c1, c2) => c1.SeriesIndex.CompareTo(c2.SeriesIndex));
+ var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
+ seriesEntry.UpdateLibraryBook(seriesBook);
+ }
+
+ //Series entry must be expanded so its child can
+ //be placed in the correct position beneath it.
+ var isExpanded = seriesEntry.Liberate.Expanded;
+ bindingList.ExpandItem(seriesEntry);
+
+ //Add episode to the grid beneath the parent
+ int seriesIndex = bindingList.IndexOf(seriesEntry);
+ int episodeIndex = seriesEntry.Children.IndexOf(episodeEntry);
+ bindingList.Insert(seriesIndex + 1 + episodeIndex, episodeEntry);
+
+ if (isExpanded)
+ bindingList.ExpandItem(seriesEntry);
+ else
+ bindingList.CollapseItem(seriesEntry);
+ }
+ else
+ existingEpisodeEntry.UpdateLibraryBook(episodeBook);
+ }
+
+ #endregion
+
+ #region Filter
+
+ public void Filter(string searchString)
+ {
+ if (bindingList is null) return;
+
+ int visibleCount = bindingList.Count;
+
+ if (string.IsNullOrEmpty(searchString))
+ syncBindingSource.RemoveFilter();
+ else
+ syncBindingSource.Filter = searchString;
+
+ if (visibleCount != bindingList.Count)
+ VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
+ }
+
+ #endregion
+
+ #region Column Customizations
+
+ private void ProductsGrid_Load(object sender, EventArgs e)
+ {
+ //https://stackoverflow.com/a/4498512/3335599
+ if (System.ComponentModel.LicenseManager.UsageMode == System.ComponentModel.LicenseUsageMode.Designtime) return;
+
+ gridEntryDataGridView.ColumnWidthChanged += gridEntryDataGridView_ColumnWidthChanged;
+ gridEntryDataGridView.ColumnDisplayIndexChanged += gridEntryDataGridView_ColumnDisplayIndexChanged;
+
+ showHideColumnsContextMenuStrip.Items.Add(new ToolStripLabel("Show / Hide Columns"));
+ showHideColumnsContextMenuStrip.Items.Add(new ToolStripSeparator());
+
+ //Restore Grid Display Settings
+ var config = Configuration.Instance;
+ var gridColumnsVisibilities = config.GridColumnsVisibilities;
+ var gridColumnsWidths = config.GridColumnsWidths;
+ var displayIndices = config.GridColumnsDisplayIndices;
+
+ var cmsKiller = new ContextMenuStrip();
+
+ foreach (DataGridViewColumn column in gridEntryDataGridView.Columns)
+ {
+ var itemName = column.DataPropertyName;
+ var visible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
+
+ var menuItem = new ToolStripMenuItem(column.HeaderText)
+ {
+ Checked = visible,
+ Tag = itemName
+ };
+ menuItem.Click += HideMenuItem_Click;
+ showHideColumnsContextMenuStrip.Items.Add(menuItem);
+
+ //Only set column widths for user resizable columns.
+ //Fixed column widths are set by setGridScale()
+ if (column.Resizable is not DataGridViewTriState.False)
+ column.Width = gridColumnsWidths.GetValueOrDefault(itemName, this.DpiScale(column.Width));
+
+ column.MinimumWidth = 10;
+ column.HeaderCell.ContextMenuStrip = showHideColumnsContextMenuStrip;
+ column.Visible = visible;
+
+ //Setting a default ContextMenuStrip will allow the columns to handle the
+ //Show() event so it is not passed up to the _dataGridView.ContextMenuStrip.
+ //This allows the ContextMenuStrip to be shown if right-clicking in the gray
+ //background of _dataGridView but not shown if right-clicking inside cells.
+ column.ContextMenuStrip = cmsKiller;
+ }
+
+ //We must set DisplayIndex properties in ascending order
+ foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
+ {
+ var column = gridEntryDataGridView.Columns
+ .Cast()
+ .SingleOrDefault(c => c.DataPropertyName == itemName);
+
+ if (column is null) continue;
+
+ column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index);
+ }
+
+ //Remove column is always first;
+ removeGVColumn.DisplayIndex = 0;
+ removeGVColumn.Visible = false;
+ removeGVColumn.ValueType = typeof(bool?);
+ removeGVColumn.FalseValue = false;
+ removeGVColumn.TrueValue = true;
+ removeGVColumn.IndeterminateValue = null;
+ }
+
+ private void HideMenuItem_Click(object sender, EventArgs e)
+ {
+ var menuItem = sender as ToolStripMenuItem;
+ var propertyName = menuItem.Tag as string;
+
+ var column = gridEntryDataGridView.Columns
+ .Cast()
+ .FirstOrDefault(c => c.DataPropertyName == propertyName);
+
+ if (column != null)
+ {
+ var visible = menuItem.Checked;
+ menuItem.Checked = !visible;
+ column.Visible = !visible;
+
+ var config = Configuration.Instance;
+
+ var dictionary = config.GridColumnsVisibilities;
+ dictionary[propertyName] = column.Visible;
+ config.GridColumnsVisibilities = dictionary;
+ }
+ }
+
+ private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
+ {
+ var config = Configuration.Instance;
+
+ var dictionary = config.GridColumnsDisplayIndices;
+ dictionary[e.Column.DataPropertyName] = e.Column.DisplayIndex;
+ config.GridColumnsDisplayIndices = dictionary;
+ }
+
+ private void gridEntryDataGridView_CellToolTipTextNeeded(object sender, DataGridViewCellToolTipTextNeededEventArgs e)
+ {
+ if (e.ColumnIndex == descriptionGVColumn.Index)
+ e.ToolTipText = "Click to see full description";
+ else if (e.ColumnIndex == coverGVColumn.Index)
+ e.ToolTipText = "Click to see full size";
+ }
+
+ private void gridEntryDataGridView_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
+ {
+ var config = Configuration.Instance;
+
+ var dictionary = config.GridColumnsWidths;
+ dictionary[e.Column.DataPropertyName] = e.Column.Width;
+ config.GridColumnsWidths = dictionary;
+ }
+
+ #endregion
+
+ private void gridEntryDataGridView_RowsAdded(object sender, DataGridViewRowsAddedEventArgs e)
+ {
+ var grid = sender as DataGridView;
+
+ // hide "Add to Playlist" button for series.
+ for (int row = 0; row < grid.Rows.Count; row++)
+ {
+ if (grid.Rows[row].DataBoundItem is ISeriesEntry)
+ grid[playlistColumn.Index, row] = GetEmptyCell();
+ else
+ grid[playlistColumn.Index, row] = GetPlaylistButtonCell();
+ }
+
+ if (SystemInformation.HighContrast)
+ {
+ // Correct high contrast call background colors.
+ for (int row = 0; row < grid.Rows.Count; row++)
+ for (int col = 0; col < grid.Columns.Count; col++)
+ {
+ if (col > playlistColumn.Index &&
+ grid.Rows[row].DataBoundItem is ILibraryBookEntry b &&
+ b.Parent != null)
+ grid[col, row].Style.BackColor = Color.FromArgb(32, 32, 32);
+ else
+ grid[col, row].Style.BackColor = SystemColors.Control;
+ }
+ }
+ }
+
+ private DataGridViewCell GetPlaylistButtonCell()
+ {
+ return new DataGridViewButtonCell
+ {
+ Style = new DataGridViewCellStyle
+ {
+ Alignment = DataGridViewContentAlignment.MiddleLeft,
+ Padding = new Padding(8),
+ WrapMode = DataGridViewTriState.False,
+ }
+ };
+ }
+
+ private DataGridViewCell GetEmptyCell() => new DataGridViewTextBoxCell
+ {
+ Style = new DataGridViewCellStyle
+ {
+ Alignment = DataGridViewContentAlignment.MiddleLeft,
+ SelectionForeColor = Color.Transparent,
+ ForeColor = Color.Transparent,
+ WrapMode = DataGridViewTriState.False
+ }
+ };
+
+ private void gridEntryDataGridView_CellClick(object sender, DataGridViewCellEventArgs e)
+ {
+ var grid = sender as DataGridView;
+
+ if (e.ColumnIndex == playlistColumn.Index &&
+ grid.Rows[e.RowIndex].DataBoundItem is ILibraryBookEntry book)
+ {
+ if (_player.IsInPlaylist(book))
+ _player.RemoveFromPlaylist(book);
+ else
+ _player.AddToPlaylist(book);
+ }
+ }
+
+ private void InvalidateBookEntry(ILibraryBookEntry bookEntry)
+ {
+
+ for (int row = 0; row < gridEntryDataGridView.Rows.Count; row++)
+ {
+ if (gridEntryDataGridView.Rows[row].DataBoundItem is ILibraryBookEntry be &&
+ be.AudibleProductId == bookEntry.AudibleProductId)
+ {
+ //bindingList.ResetItem(row);
+ //gridEntryDataGridView.InvalidateRow(row);
+ playlistColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
+ playlistColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
+ return;
+ }
+ }
+ }
+ }
}
diff --git a/Source/LibationWinForms/GridView/ProductsGrid.resx b/Source/LibationWinForms/GridView/ProductsGrid.resx
index 19057fc30..e43aa19c1 100644
--- a/Source/LibationWinForms/GridView/ProductsGrid.resx
+++ b/Source/LibationWinForms/GridView/ProductsGrid.resx
@@ -1,7 +1,7 @@
@@ -58,7 +118,7 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
- 17, 17
+ 513, 31
@@ -636,6 +696,30 @@
True
+
+ True
+
+
+ True
+
+
+ True
+
+
+ True
+
+
+ True
+
+
+ True
+
+
+ True
+
+
+ True
+
133, 17
diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs
index 2378c0e34..13d3e6a6a 100644
--- a/Source/LibationWinForms/Program.cs
+++ b/Source/LibationWinForms/Program.cs
@@ -2,17 +2,21 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using AppScaffolding;
using DataLayer;
using LibationFileManager;
+using LibationUiBase;
+using LibationUiBase.ViewModels;
+using LibationUiBase.ViewModels.Player;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
- static class Program
+ static class Program
{
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
@@ -84,12 +88,31 @@ static void Main()
// global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd
postLoggingGlobalExceptionHandling();
- var form1 = new Form1();
+ RegisterTypes();
+ ServiceLocator.AddCommonServicesAndBuild();
+
+ var form1 = new Form1();
form1.Load += async (_, _) => await form1.InitLibraryAsync(await libraryLoadTask);
Application.Run(form1);
}
- private static void RunInstaller(Configuration config)
+ private static void RegisterTypes()
+ {
+ ServiceLocator.RegisterTransient();
+ ServiceLocator.RegisterSingleton();
+ ServiceLocator.RegisterTransient();
+
+ // Register VMs here only.
+ foreach (var type in Assembly.GetExecutingAssembly().GetExportedTypes())
+ {
+ if (type.IsSubclassOf(typeof(ViewModelBase)) && !type.IsAbstract)
+ ServiceLocator.RegisterTransient(type);
+ }
+
+ // Add more types as needed here.
+ }
+
+ private static void RunInstaller(Configuration config)
{
// all returns should be preceded by either:
// - if config.LibationSettingsAreValid
diff --git a/Source/LibationWinForms/WinFormsCanExecuteChanged.cs b/Source/LibationWinForms/WinFormsCanExecuteChanged.cs
new file mode 100644
index 000000000..01c44c4d3
--- /dev/null
+++ b/Source/LibationWinForms/WinFormsCanExecuteChanged.cs
@@ -0,0 +1,15 @@
+using LibationUiBase.ViewModels;
+using System;
+
+namespace LibationWinForms
+{
+ public class WinFormsCanExecuteChanged : ICanExecuteChanged
+ {
+ public event EventHandler Event;
+
+ public void Raise()
+ {
+ Event?.Invoke(null, EventArgs.Empty);
+ }
+ }
+}
diff --git a/Source/SystemInformation.cs b/Source/SystemInformation.cs
new file mode 100644
index 000000000..1cd4b58a1
--- /dev/null
+++ b/Source/SystemInformation.cs
@@ -0,0 +1,9 @@
+using System;
+
+public static class Theme
+{
+ [DllImport("UXTheme.dll", SetLastError = true, EntryPoint = "#138")]
+ private static extern bool ShouldSystemUseDarkMode();
+
+ public static bool IsDarkMode => SystemInformation.HighContrast || ShouldSystemUseDarkMode();
+}