diff --git a/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs
index b481dda4..d19b222d 100644
--- a/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs
+++ b/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs
@@ -1,79 +1,17 @@
using Avalonia.Controls;
-using Avalonia.Interactivity;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
-using System.Reflection;
namespace LibationWinForms.AvaloniaUI.Controls
{
- /// The purpose of this extension it to immediately commit any check
- /// state changes to the viewmodel. There must be a better way to do this, but
- /// I sure as shit can't find it.
public partial class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
{
- Func _owningGrid_get;
- Func _endCellEdit;
- Func _waitForLostFocus;
- public DataGrid OwningGrid
- {
- get
- {
- if (_owningGrid_get == null)
- {
- var pi = typeof(DataGridColumn).GetProperty(nameof(OwningGrid), BindingFlags.NonPublic | BindingFlags.Instance);
- var mi = pi.GetGetMethod(true);
- _owningGrid_get = mi.CreateDelegate>(this);
- }
- return _owningGrid_get();
- }
- }
-
- public Func WaitForLostFocus
- {
- get
- {
- if (_endCellEdit == null)
- {
- var mi = typeof(DataGrid).GetMethod(nameof(WaitForLostFocus), BindingFlags.NonPublic | BindingFlags.Instance);
- _waitForLostFocus = mi.CreateDelegate>(OwningGrid);
- }
- return _waitForLostFocus;
- }
- }
-
- public Func EndCellEdit
- {
- get
- {
- if (_endCellEdit == null)
- {
- var mi = typeof(DataGrid).GetMethod(nameof(EndCellEdit), BindingFlags.NonPublic | BindingFlags.Instance);
- _endCellEdit = mi.CreateDelegate>(OwningGrid);
- }
- return _endCellEdit;
- }
- }
-
protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
{
+ //Only SeriesEntry types have three-state checks, individual LibraryEntry books are binary.
var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox;
- ele.Checked += EditingElement_Checked;
- ele.Unchecked += EditingElement_Checked;
- ele.Indeterminate += EditingElement_Checked;
+ ele.IsThreeState = dataItem is SeriesEntrys2;
return ele;
}
-
- private void EditingElement_Checked(object sender, RoutedEventArgs e)
- {
- if (sender is CheckBox cbox && cbox.DataContext is GridEntry2 gentry)
- {
- var check = cbox.IsChecked;
- WaitForLostFocus(() =>
- {
- EndCellEdit(DataGridEditAction.Cancel, true, true, false);
- gentry.Remove = check;
- });
- }
- }
}
}
diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntry2.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntry2.cs
index 3f866bf3..2b85abd6 100644
--- a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntry2.cs
+++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntry2.cs
@@ -1,4 +1,5 @@
-using DataLayer;
+using Avalonia.Media;
+using DataLayer;
using Dinah.Core;
using Dinah.Core.DataBinding;
using Dinah.Core.Drawing;
@@ -28,39 +29,32 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
[Browsable(false)] public float SeriesIndex { get; protected set; }
[Browsable(false)] public string LongDescription { get; protected set; }
[Browsable(false)] public abstract DateTime DateAdded { get; }
+ [Browsable(false)] public int ListIndex { get; set; }
[Browsable(false)] protected Book Book => LibraryBook.Book;
#region Model properties exposed to the view
private Avalonia.Media.Imaging.Bitmap _cover;
- private string _purchaseDate;
- private string _series;
- private string _title;
- private string _length;
- private string _authors;
- private string _narrators;
- private string _category;
- private string _misc;
- private string _description;
- private string _productRating;
- private string _myRating;
public Avalonia.Media.Imaging.Bitmap Cover { get => _cover; protected set { this.RaiseAndSetIfChanged(ref _cover, value); } }
- public string PurchaseDate { get => _purchaseDate; protected set { this.RaiseAndSetIfChanged(ref _purchaseDate, value); } }
- public string Series { get => _series; protected set { this.RaiseAndSetIfChanged(ref _series, value); } }
- public string Title { get => _title; protected set { this.RaiseAndSetIfChanged(ref _title, value); } }
- public string Length { get => _length; protected set { this.RaiseAndSetIfChanged(ref _length, value); } }
- public string Authors { get => _authors; protected set { this.RaiseAndSetIfChanged(ref _authors, value); } }
- public string Narrators { get => _narrators; protected set { this.RaiseAndSetIfChanged(ref _narrators, value); } }
- public string Category { get => _category; protected set { this.RaiseAndSetIfChanged(ref _category, value); } }
- public string Misc { get => _misc; protected set { this.RaiseAndSetIfChanged(ref _misc, value); } }
- public string Description { get => _description; protected set { this.RaiseAndSetIfChanged(ref _description, value); } }
- public string ProductRating { get => _productRating; protected set { this.RaiseAndSetIfChanged(ref _productRating, value); } }
- public string MyRating { get => _myRating; protected set { this.RaiseAndSetIfChanged(ref _myRating, value); } }
+ public string PurchaseDate { get; protected set; }
+ public string Series { get; protected set; }
+ public string Title { get; protected set; }
+ public string Length { get; protected set; }
+ public string Authors { get; protected set; }
+ public string Narrators { get; protected set; }
+ public string Category { get; protected set; }
+ public string Misc { get; protected set; }
+ public string Description { get; protected set; }
+ public string ProductRating { get; protected set; }
+ public string MyRating { get; protected set; }
protected bool? _remove = false;
public abstract bool? Remove { get; set; }
public abstract LiberateButtonStatus2 Liberate { get; }
public abstract BookTags BookTags { get; }
+ public abstract bool IsSeries { get; }
+ public abstract bool IsEpisode { get; }
+ public abstract bool IsBook { get; }
#endregion
diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs
index 48b3f90d..3448e88b 100644
--- a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs
+++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs
@@ -48,6 +48,13 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
base.Remove(entry);
}
+ public void ReplaceList(IEnumerable newItems)
+ {
+ Items.Clear();
+ ((List)Items).AddRange(newItems);
+ ResetCollection();
+ }
+
protected override void InsertItem(int index, GridEntry2 item)
{
FilterRemoved.Remove(item);
@@ -120,10 +127,22 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
{
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
{
- Remove(episode);
+ /*
+ * Bypass ObservationCollection's InsertItem methos so that CollectionChanged isn't
+ * fired. When adding many items at once, Avalonia's CollectionChanged event handler
+ * causes serious performance problems. And unfotrunately, Avalonia doesn't respect
+ * the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
+ * overload that would fire only once for all changed items.
+ *
+ * Doing this requires resetting the list so the view knows it needs to rebuild its display.
+ */
+
+ FilterRemoved.Add(episode);
+ Items.Remove(episode);
}
sEntry.Liberate.Expanded = false;
+ ResetCollection();
}
public void ExpandItem(SeriesEntrys2 sEntry)
@@ -134,10 +153,23 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
{
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
{
- InsertItem(++sindex, episode);
+ /*
+ * Bypass ObservationCollection's InsertItem methos so that CollectionChanged isn't
+ * fired. When adding many items at once, Avalonia's CollectionChanged event handler
+ * causes serious performance problems. And unfotrunately, Avalonia doesn't respect
+ * the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
+ * overload that would fire only once for all changed items.
+ *
+ * Doing this requires resetting the list so the view knows it needs to rebuild its display.
+ */
+
+ FilterRemoved.Remove(episode);
+ Items.Insert(++sindex, episode);
}
}
+
sEntry.Liberate.Expanded = true;
+ ResetCollection();
}
#endregion
diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/LiberateButtonStatus2.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/LiberateButtonStatus2.cs
index f17c519c..2f506133 100644
--- a/Source/LibationWinForms/AvaloniaUI/ViewModels/LiberateButtonStatus2.cs
+++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/LiberateButtonStatus2.cs
@@ -8,6 +8,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class LiberateButtonStatus2 : ViewModelBase, IComparable
{
+ public LiberateButtonStatus2(bool isSeries)
+ {
+ IsSeries = isSeries;
+ }
public LiberatedStatus BookStatus { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
@@ -22,11 +26,11 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
this.RaisePropertyChanged(nameof(ToolTip));
}
}
- public bool IsSeries { get; init; }
+ private bool IsSeries { get; }
public Bitmap Image => GetLiberateIcon();
public string ToolTip => GetTooltip();
- static Dictionary images = new();
+ static Dictionary iconCache = new();
/// Defines the Liberate column's sorting behavior
public int CompareTo(object obj)
@@ -106,14 +110,14 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
private static Bitmap GetFromResources(string rescName)
{
- if (images.ContainsKey(rescName)) return images[rescName];
+ if (iconCache.ContainsKey(rescName)) return iconCache[rescName];
var memoryStream = new System.IO.MemoryStream();
((System.Drawing.Bitmap)Properties.Resources.ResourceManager.GetObject(rescName)).Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
memoryStream.Position = 0;
- images[rescName] = new Bitmap(memoryStream);
- return images[rescName];
+ iconCache[rescName] = new Bitmap(memoryStream);
+ return iconCache[rescName];
}
}
}
diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/LibraryBookEntry2.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/LibraryBookEntry2.cs
index 7e46c741..0b5c2f61 100644
--- a/Source/LibationWinForms/AvaloniaUI/ViewModels/LibraryBookEntry2.cs
+++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/LibraryBookEntry2.cs
@@ -1,7 +1,6 @@
using ApplicationServices;
using DataLayer;
using Dinah.Core;
-using LibationWinForms.GridView;
using ReactiveUI;
using System;
using System.Collections.Generic;
@@ -27,7 +26,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
get => _remove;
set
{
- _remove = value.HasValue ? value.Value : false;
+ _remove = value ?? false;
Parent?.ChildRemoveUpdate();
this.RaisePropertyChanged(nameof(Remove));
@@ -45,32 +44,22 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
_pdfStatus = LibraryCommands.Pdf_Status(LibraryBook.Book);
lastStatusUpdate = DateTime.Now;
}
- return new LiberateButtonStatus2 { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
+ return new LiberateButtonStatus2(IsSeries) { BookStatus = _bookStatus, PdfStatus = _pdfStatus };
}
}
public override BookTags BookTags => new() { Tags = string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated) };
+ public override bool IsSeries => false;
+ public override bool IsEpisode => Parent is not null;
+ public override bool IsBook => Parent is null;
+
#endregion
public LibraryBookEntry2(LibraryBook libraryBook)
- {
- setLibraryBook(libraryBook);
- LoadCover();
- }
-
- public void UpdateLibraryBook(LibraryBook libraryBook)
- {
- if (AudibleProductId != libraryBook.Book.AudibleProductId)
- throw new Exception("Invalid grid entry update. IDs must match");
-
- UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
- setLibraryBook(libraryBook);
- }
-
- private void setLibraryBook(LibraryBook libraryBook)
{
LibraryBook = libraryBook;
+ LoadCover();
Title = Book.Title;
Series = Book.SeriesNames();
diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/ProductsDisplayViewModel.cs
index d0cd021f..de7dfb21 100644
--- a/Source/LibationWinForms/AvaloniaUI/ViewModels/ProductsDisplayViewModel.cs
+++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/ProductsDisplayViewModel.cs
@@ -13,7 +13,21 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
public class ProductsDisplayViewModel : ViewModelBase
{
public GridEntryBindingList2 GridEntries { get; set; }
+ public DataGridCollectionView GridCollectionView { get; set; }
public ProductsDisplayViewModel(IEnumerable dbBooks)
+ {
+ GridEntries = new GridEntryBindingList2(CreateGridEntries(dbBooks));
+ GridEntries.CollapseAll();
+
+ /*
+ * Would be nice to use built-in groups, but Avalonia doesn't yet let you customize the row group header.
+ *
+ GridCollectionView = new DataGridCollectionView(GridEntries);
+ GridCollectionView.GroupDescriptions.Add(new CustonGroupDescription());
+ */
+ }
+
+ public static IEnumerable CreateGridEntries(IEnumerable dbBooks)
{
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
@@ -36,9 +50,22 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
}
-
- GridEntries = new GridEntryBindingList2(geList.OrderByDescending(e => e.DateAdded));
- GridEntries.CollapseAll();
+ return geList.OrderByDescending(e => e.DateAdded);
+ }
+ }
+ class CustonGroupDescription : DataGridGroupDescription
+ {
+ public override object GroupKeyFromItem(object item, int level, CultureInfo culture)
+ {
+ if (item is SeriesEntrys2 sEntry)
+ return sEntry;
+ else if (item is LibraryBookEntry2 lbEntry && lbEntry.Parent is SeriesEntrys2 sEntry2)
+ return sEntry2;
+ else return null;
+ }
+ public override bool KeysMatch(object groupKey, object itemKey)
+ {
+ return base.KeysMatch(groupKey, itemKey);
}
}
}
diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/RowComparer.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/RowComparer.cs
index cdc6c274..9738a18c 100644
--- a/Source/LibationWinForms/AvaloniaUI/ViewModels/RowComparer.cs
+++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/RowComparer.cs
@@ -3,21 +3,20 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.Reflection;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
///
/// This compare class ensures that all top-level grid entries (standalone books or series parents)
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
- /// sorted by series index, ascending.
+ /// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
+ /// properties when 2 items compare equal.
///
- internal class RowComparer : IComparer, IComparer
+ internal class RowComparer : IComparer
{
- private static readonly System.Reflection.PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- private static readonly System.Reflection.PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
+ private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
public DataGridColumn Column { get; init; }
public string PropertyName { get; private set; }
@@ -34,7 +33,6 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
PropertyName = propertyName;
}
-
public int Compare(object x, object y)
{
if (x is null && y is not null) return -1;
@@ -54,71 +52,26 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
if (geB is LibraryBookEntry2 lbB && lbB.Parent is SeriesEntrys2 seB)
parentB = seB;
- if (geA is SeriesEntrys2 && geB is SeriesEntrys2)
- {
- //Both are parents. Make sure they never compare equal.
- var comparison = InternalCompare(geA, geB);
- if (comparison == 0)
- {
- var propBackup = PropertyName;
- PropertyName = nameof(GridEntry2.Series);
- comparison = InternalCompare(geA, geB);
- PropertyName = propBackup;
- return comparison;
- }
- return comparison;
- }
-
-
-
- //both a and b are standalone
+ //both a and b are top-level grid entries
if (parentA is null && parentB is null)
return InternalCompare(geA, geB);
- //a is a standalone, b is a child
+ //a is top-level, b is a child
if (parentA is null && parentB is not null)
{
// b is a child of a, parent is always first
if (parentB == geA)
return SortDirection is ListSortDirection.Ascending ? -1 : 1;
- else if (geA is SeriesEntrys2)
- {
- //Both are parents. Make sure they never compare equal.
- var comparison = InternalCompare(geA, parentB);
- if (comparison == 0)
- {
- var propBackup = PropertyName;
- PropertyName = nameof(GridEntry2.Series);
- comparison = InternalCompare(geA, parentB);
- PropertyName = propBackup;
- return comparison;
- }
- return comparison;
- }
else
return InternalCompare(geA, parentB);
}
- //a is a child, b is a standalone
+ //a is a child, b is a top-level
if (parentA is not null && parentB is null)
{
// a is a child of b, parent is always first
if (parentA == geB)
return SortDirection is ListSortDirection.Ascending ? 1 : -1;
- else if (geB is SeriesEntrys2)
- {
- //Both are parents. Make sure they never compare equal.
- var comparison = InternalCompare(parentA, geB);
- if (comparison == 0)
- {
- var propBackup = PropertyName;
- PropertyName = nameof(GridEntry2.Series);
- comparison = InternalCompare(parentA, geB);
- PropertyName = propBackup;
- return comparison;
- }
- return comparison;
- }
else
return InternalCompare(parentA, geB);
}
@@ -127,17 +80,8 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
if (parentA == parentB)
return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (SortDirection is ListSortDirection.Ascending ? 1 : -1);
- //a and b are children of different series. Make sure their parents never compare equal.
- var comparison2 = InternalCompare(parentA, parentB);
- if (comparison2 == 0)
- {
- var propBackup = PropertyName;
- PropertyName = nameof(GridEntry2.Series);
- comparison2 = InternalCompare(parentA, parentB);
- PropertyName = propBackup;
- return comparison2;
- }
- return comparison2;
+ //a and b are children of different series.
+ return Compare(parentA, parentB);
}
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
@@ -149,17 +93,14 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
var val1 = x.GetMemberValue(PropertyName);
var val2 = y.GetMemberValue(PropertyName);
- return x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
- }
+ var compareResult = x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
- public int CompareTo(GridEntry2 other)
- {
- return Compare(this, other);
- }
-
- public int Compare(GridEntry2 x, GridEntry2 y)
- {
- return Compare((object)x, (object)y) * (SortDirection is ListSortDirection.Ascending ? 1 : -1);
+ //If items compare equal, compare them by their positions in the the list.
+ //This is how you achieve a stable sort.
+ if (compareResult == 0)
+ return x.ListIndex.CompareTo(y.ListIndex);
+ else
+ return compareResult;
}
}
}
diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/SeriesEntrys2.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/SeriesEntrys2.cs
index 21c41bcb..4cec5386 100644
--- a/Source/LibationWinForms/AvaloniaUI/ViewModels/SeriesEntrys2.cs
+++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/SeriesEntrys2.cs
@@ -1,4 +1,5 @@
-using DataLayer;
+using Avalonia.Media;
+using DataLayer;
using Dinah.Core;
using ReactiveUI;
using System;
@@ -31,7 +32,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
get => _remove;
set
{
- _remove = value.HasValue ? value.Value : false;
+ _remove = value ?? false;
suspendCounting = true;
@@ -46,39 +47,28 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
public override LiberateButtonStatus2 Liberate { get; }
public override BookTags BookTags { get; } = new();
+ public override bool IsSeries => true;
+ public override bool IsEpisode => false;
+ public override bool IsBook => false;
+
#endregion
- private SeriesEntrys2(LibraryBook parent)
+ public SeriesEntrys2(LibraryBook parent, IEnumerable children)
{
- Liberate = new LiberateButtonStatus2 { IsSeries = true };
+ Liberate = new LiberateButtonStatus2(IsSeries);
SeriesIndex = -1;
LibraryBook = parent;
- LoadCover();
- }
- public SeriesEntrys2(LibraryBook parent, IEnumerable children) : this(parent)
- {
+ LoadCover();
+
Children = children
.Select(c => new LibraryBookEntry2(c) { Parent = this })
.OrderBy(c => c.SeriesIndex)
.ToList();
- UpdateSeries(parent);
- }
-
- public SeriesEntrys2(LibraryBook parent, LibraryBook child) : this(parent)
- {
- Children = new() { new LibraryBookEntry2(child) { Parent = this } };
- UpdateSeries(parent);
- }
-
- public void UpdateSeries(LibraryBook parent)
- {
- LibraryBook = parent;
Title = Book.Title;
Series = Book.SeriesNames();
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
- PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
Authors = Book.AuthorNames();
Narrators = Book.NarratorNames();
@@ -87,10 +77,12 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
LongDescription = GetDescriptionDisplay(Book);
Description = TrimTextToWord(LongDescription, 62);
+ PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
}
+
#region Data Sorting
/// Create getters for all member object values by name
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.VisibleBooks.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.VisibleBooks.axaml.cs
index 4523ce71..8b45239b 100644
--- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.VisibleBooks.axaml.cs
+++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.VisibleBooks.axaml.cs
@@ -40,7 +40,7 @@ namespace LibationWinForms.AvaloniaUI.Views
processBookQueue1.AddDownloadDecrypt(
productsDisplay
- .GetVisible()
+ .GetVisibleBookEntries()
.UnLiberated()
);
}
@@ -56,7 +56,7 @@ namespace LibationWinForms.AvaloniaUI.Views
if (result != System.Windows.Forms.DialogResult.OK)
return;
- var visibleLibraryBooks = productsDisplay.GetVisible();
+ var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
@@ -78,7 +78,7 @@ namespace LibationWinForms.AvaloniaUI.Views
if (result != System.Windows.Forms.DialogResult.OK)
return;
- var visibleLibraryBooks = productsDisplay.GetVisible();
+ var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
@@ -95,7 +95,7 @@ namespace LibationWinForms.AvaloniaUI.Views
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
- var visibleLibraryBooks = productsDisplay.GetVisible();
+ var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
@@ -121,13 +121,13 @@ namespace LibationWinForms.AvaloniaUI.Views
});
//Not used for anything?
- var notLiberatedCount = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
+ var notLiberatedCount = productsDisplay.GetVisibleBookEntries().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
await Task.Run(setLiberatedVisibleMenuItem);
}
void setLiberatedVisibleMenuItem()
{
- var notLiberated = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
+ var notLiberated = productsDisplay.GetVisibleBookEntries().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
Dispatcher.UIThread.Post(() =>
{
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml
index 47c66173..fcdb7ff1 100644
--- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml
+++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml
@@ -5,6 +5,7 @@
xmlns:vm="clr-namespace:LibationWinForms.AvaloniaUI.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:LibationWinForms.AvaloniaUI.Views"
+ xmlns:prgid="clr-namespace:LibationWinForms.AvaloniaUI.Views.ProductsGrid"
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="2000" d:DesignHeight="700"
x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow" Title="MainWindow">
@@ -145,7 +146,7 @@
- Number of visible rows has changed
- public event EventHandler VisibleCountChanged;
- public event EventHandler RemovableCountChanged;
- public event EventHandler LiberateClicked;
- public event EventHandler InitialLoaded;
-
- private ProductsDisplayViewModel _viewModel;
- private GridEntryBindingList2 bindingList => _viewModel.GridEntries;
- private IEnumerable GetAllBookEntries()
- => bindingList.AllItems().BookEntries();
-
- internal List GetVisible()
- => bindingList
- .BookEntries()
- .Select(lbe => lbe.LibraryBook)
- .ToList();
-
-
- DataGridColumn removeGVColumn;
- DataGridColumn liberateGVColumn;
- DataGridColumn coverGVColumn;
- DataGridColumn titleGVColumn;
- DataGridColumn authorsGVColumn;
- DataGridColumn narratorsGVColumn;
- DataGridColumn lengthGVColumn;
- DataGridColumn seriesGVColumn;
- DataGridColumn descriptionGVColumn;
- DataGridColumn categoryGVColumn;
- DataGridColumn productRatingGVColumn;
- DataGridColumn purchaseDateGVColumn;
- DataGridColumn myRatingGVColumn;
- DataGridColumn miscGVColumn;
- DataGridColumn tagAndDetailsGVColumn;
-
- #region Init
-
- public ProductsDisplay2()
- {
- InitializeComponent();
-
-
- if (Design.IsDesignMode)
- {
- using var context = DbContexts.GetContext();
- var book = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
- productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(new List { book });
- return;
- }
- }
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
-
- productsGrid = this.FindControl(nameof(productsGrid));
- productsGrid.Sorting += ProductsGrid_Sorting;
- productsGrid.CanUserSortColumns = true;
- productsGrid.LoadingRow += ProductsGrid_LoadingRow;
-
- removeGVColumn = productsGrid.Columns[0];
- liberateGVColumn = productsGrid.Columns[1];
- coverGVColumn = productsGrid.Columns[2];
- titleGVColumn = productsGrid.Columns[3];
- authorsGVColumn = productsGrid.Columns[4];
- narratorsGVColumn = productsGrid.Columns[5];
- lengthGVColumn = productsGrid.Columns[6];
- seriesGVColumn = productsGrid.Columns[7];
- descriptionGVColumn = productsGrid.Columns[8];
- categoryGVColumn = productsGrid.Columns[9];
- productRatingGVColumn = productsGrid.Columns[10];
- purchaseDateGVColumn = productsGrid.Columns[11];
- myRatingGVColumn = productsGrid.Columns[12];
- miscGVColumn = productsGrid.Columns[13];
- tagAndDetailsGVColumn = productsGrid.Columns[14];
-
- RegisterCustomColumnComparers();
- }
-
- #endregion
-
- #region Apply Background Brush Style to Series Books Rows
-
- private static object tagObj = new();
- private void ProductsGrid_LoadingRow(object sender, DataGridRowEventArgs e)
- {
- if (e.Row.Tag == tagObj)
- return;
- e.Row.Tag = tagObj;
-
- static IBrush GetRowColor(DataGridRow row)
- => row.DataContext is GridEntry2 gEntry
- && gEntry is LibraryBookEntry2 lbEntry
- && lbEntry.Parent is not null
- ? App.SeriesEntryGridBackgroundBrush
- : null;
-
- e.Row.Background = GetRowColor(e.Row);
- e.Row.DataContextChanged += (sender, e) =>
- {
- var row = sender as DataGridRow;
- row.Background = GetRowColor(row);
- };
- }
-
- #endregion
-
- #region Filter
-
- public void Filter(string searchString)
- {
- int visibleCount = bindingList.Count;
-
- if (string.IsNullOrEmpty(searchString))
- bindingList.RemoveFilter();
- else
- bindingList.Filter = searchString;
-
- if (visibleCount != bindingList.Count)
- VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
-
- //Re-sort after filtering
- ReSort();
- }
-
- #endregion
-
- #region Sorting
-
- private void RegisterCustomColumnComparers()
- {
-
- removeGVColumn.CustomSortComparer = new RowComparer(removeGVColumn);
- liberateGVColumn.CustomSortComparer = new RowComparer(liberateGVColumn);
- titleGVColumn.CustomSortComparer = new RowComparer(titleGVColumn);
- authorsGVColumn.CustomSortComparer = new RowComparer(authorsGVColumn);
- narratorsGVColumn.CustomSortComparer = new RowComparer(narratorsGVColumn);
- lengthGVColumn.CustomSortComparer = new RowComparer(lengthGVColumn);
- seriesGVColumn.CustomSortComparer = new RowComparer(seriesGVColumn);
- descriptionGVColumn.CustomSortComparer = new RowComparer(descriptionGVColumn);
- categoryGVColumn.CustomSortComparer = new RowComparer(categoryGVColumn);
- productRatingGVColumn.CustomSortComparer = new RowComparer(productRatingGVColumn);
- purchaseDateGVColumn.CustomSortComparer = new RowComparer(purchaseDateGVColumn);
- myRatingGVColumn.CustomSortComparer = new RowComparer(myRatingGVColumn);
- miscGVColumn.CustomSortComparer = new RowComparer(miscGVColumn);
- tagAndDetailsGVColumn.CustomSortComparer = new RowComparer(tagAndDetailsGVColumn);
- }
-
- private void ReSort()
- {
- if (CurrentSortColumn is null)
- {
- bindingList.InternalList.Sort(new RowComparer(ListSortDirection.Descending, nameof(GridEntry2.DateAdded)));
- bindingList.ResetCollection();
- }
- else
- CurrentSortColumn.Sort(((RowComparer)CurrentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
- }
-
-
- private DataGridColumn CurrentSortColumn;
-
- private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
- {
- var comparer = e.Column.CustomSortComparer as RowComparer;
- //Force the comparer to get the current sort order. We can't
- //retrieve it from inside this event handler because Avalonia
- //doesn't set the property until after this event.
- comparer.SortDirection = null;
- CurrentSortColumn = e.Column;
- }
-
-
- #endregion
-
- #region Button controls
-
- public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
- {
- var button = args.Source as Button;
-
- if (button.DataContext is SeriesEntrys2 sEntry)
- {
- if (sEntry.Liberate.Expanded)
- bindingList.CollapseItem(sEntry);
- else
- {
- bindingList.ExpandItem(sEntry);
- ReSort();
- }
-
- VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
- }
- else if (button.DataContext is LibraryBookEntry2 lbEntry)
- {
- LiberateClicked?.Invoke(this, lbEntry.LibraryBook);
- }
- }
-
- private GridView.ImageDisplay imageDisplay;
- public async void Cover_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
- {
- if (sender is not Image tblock || tblock.DataContext is not GridEntry2 gEntry)
- return;
-
- var picDefinition = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
- var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
-
- (_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(gEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
- var windowTitle = $"{gEntry.Title} - Cover";
-
- if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
- {
- imageDisplay = new GridView.ImageDisplay();
- imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
- imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
- imageDisplay.Show(null);
- }
-
- imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(gEntry.LibraryBook);
- imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(gEntry.LibraryBook, ".jpg"));
- imageDisplay.Text = windowTitle;
- imageDisplay.CoverPicture = initialImageBts;
- imageDisplay.CoverPicture = await picDlTask;
- }
-
- public void Description_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
- {
- if (sender is TextBlock tblock && tblock.DataContext is GridEntry2 gEntry)
- {
- var pt = tblock.Parent.PointToScreen(tblock.Parent.Bounds.TopRight);
- var displayWindow = new GridView.DescriptionDisplay
- {
- SpawnLocation = new System.Drawing.Point(pt.X, pt.Y),
- DescriptionText = gEntry.LongDescription,
- BorderThickness = 2,
- };
-
- void CloseWindow(object o, DataGridRowEventArgs e)
- {
- displayWindow.Close();
- }
- productsGrid.LoadingRow += CloseWindow;
- displayWindow.FormClosed += (_, _) =>
- {
- productsGrid.LoadingRow -= CloseWindow;
- };
-
- displayWindow.Show();
- }
- }
-
- public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
- {
- var button = args.Source as Button;
-
- if (button.DataContext is LibraryBookEntry2 lbEntry)
- {
- var bookDetailsForm = new Dialogs.BookDetailsDialog(lbEntry.LibraryBook);
- if (bookDetailsForm.ShowDialog() == System.Windows.Forms.DialogResult.OK)
- lbEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
- }
- }
-
- #endregion
-
- #region Scan and Remove Books
-
- public void CloseRemoveBooksColumn()
- => removeGVColumn.IsVisible = false;
-
- public async Task RemoveCheckedBooksAsync()
- {
- var selectedBooks = GetAllBookEntries().Where(lbe => lbe.Remove == true).ToList();
-
- if (selectedBooks.Count == 0)
- return;
-
- var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
- var result = MessageBoxLib.ShowConfirmationDialog(
- libraryBooks,
- $"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
- "Remove books from Libation?");
-
- if (result != System.Windows.Forms.DialogResult.Yes)
- return;
-
- RemoveBooks(selectedBooks);
- var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
- var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
-
- RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
- }
- public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
- {
- RemovableCountChanged?.Invoke(this, 0);
- removeGVColumn.IsVisible = true;
-
- try
- {
- if (accounts is null || accounts.Length == 0)
- return;
-
- var allBooks = GetAllBookEntries();
-
- foreach (var b in allBooks)
- b.Remove = false;
-
- var lib = allBooks
- .Select(lbe => lbe.LibraryBook)
- .Where(lb => !lb.Book.HasLiberated());
-
- var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts);
-
- var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
-
- foreach (var r in removable)
- r.Remove = true;
-
- RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
- }
- catch (Exception ex)
- {
- MessageBoxLib.ShowAdminAlert(
- null,
- "Error scanning library. You may still manually select books to remove from Libation's library.",
- "Error scanning library",
- ex);
- }
- }
-
-
- #endregion
-
- #region UI display functions
-
- public void Display()
- {
- try
- {
- // don't return early if lib size == 0. this will not update correctly if all books are removed
- var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
- if (productsGrid.DataContext is null)
- {
- productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(dbBooks);
- InitialLoaded?.Invoke(this, EventArgs.Empty);
- VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
- }
- else
- UpdateGrid(dbBooks);
- }
- catch (Exception ex)
- {
- Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay2));
- }
- }
-
- private void UpdateGrid(List dbBooks)
- {
- #region Add new or update existing grid entries
-
- //Remove filter prior to adding/updating boooks
- string existingFilter = bindingList.Filter;
- Filter(null);
-
- bindingList.SuspendFilteringOnUpdate = true;
-
- //Add absent entries to grid, or update existing entry
-
- var allEntries = bindingList.AllItems().BookEntries();
- var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
- var parentedEpisodes = dbBooks.ParentedEpisodes();
-
- foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
- {
- var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
-
- if (libraryBook.Book.IsProduct())
- AddOrUpdateBook(libraryBook, existingEntry);
- else if (parentedEpisodes.Any(lb => lb == libraryBook))
- //Only try to add or update is this LibraryBook is a know child of a parent
- AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
- }
-
- bindingList.SuspendFilteringOnUpdate = false;
-
- //Re-apply filter after adding new/updating existing books to capture any changes
- 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);
- }
-
- private void RemoveBooks(IEnumerable removedBooks)
- {
- //Remove books in series from their parents' Children list
- foreach (var removed in removedBooks.Where(b => b.Parent is not null))
- {
- removed.Parent.Children.Remove(removed);
-
- //In Avalonia, if you fire PropertyChanged with an empty or invalid property name, nothing is updated.
- //So we must notify for specific properties that we believed changed.
- removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.Length));
- removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.PurchaseDate));
- }
-
- //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.BookEntries().Count());
- }
-
- private void AddOrUpdateBook(LibraryBook book, LibraryBookEntry2 existingBookEntry)
- {
- if (existingBookEntry is null)
- // Add the new product to top
- bindingList.Insert(0, new LibraryBookEntry2(book));
- else
- // update existing
- existingBookEntry.UpdateLibraryBook(book);
- }
-
- private void AddOrUpdateEpisode(LibraryBook episodeBook, LibraryBookEntry2 existingEpisodeEntry, List seriesEntries, IEnumerable dbBooks)
- {
- if (existingEpisodeEntry is null)
- {
- LibraryBookEntry2 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 SeriesEntrys2(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(episodeBook) { Parent = seriesEntry };
- seriesEntry.Children.Add(episodeEntry);
- var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
- seriesEntry.UpdateSeries(seriesBook);
- }
-
- //Add episode to the grid beneath the parent
- int seriesIndex = bindingList.IndexOf(seriesEntry);
- bindingList.Insert(seriesIndex + 1, episodeEntry);
-
- if (seriesEntry.Liberate.Expanded)
- bindingList.ExpandItem(seriesEntry);
- else
- bindingList.CollapseItem(seriesEntry);
-
- seriesEntry.RaisePropertyChanged(nameof(SeriesEntrys2.Length));
- seriesEntry.RaisePropertyChanged(nameof(SeriesEntrys2.PurchaseDate));
- }
- else
- existingEpisodeEntry.UpdateLibraryBook(episodeBook);
- }
-
- #endregion
-
- #region Column Customizations
-
-
-
- #endregion
- }
-}
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Buttons.xaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Buttons.xaml.cs
new file mode 100644
index 00000000..bdfd6148
--- /dev/null
+++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Buttons.xaml.cs
@@ -0,0 +1,109 @@
+using Avalonia;
+using Avalonia.Controls;
+using FileLiberator;
+using LibationFileManager;
+using LibationWinForms.AvaloniaUI.ViewModels;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
+{
+ public partial class ProductsDisplay2
+ {
+
+ private GridView.ImageDisplay imageDisplay;
+ private void Configure_Buttons() { }
+
+ public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
+ {
+ var button = args.Source as Button;
+
+ if (button.DataContext is SeriesEntrys2 sEntry)
+ {
+ if (sEntry.Liberate.Expanded)
+ {
+ bindingList.CollapseItem(sEntry);
+ }
+ else
+ {
+ bindingList.ExpandItem(sEntry);
+ }
+
+ VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
+
+ //Expanding and collapsing reset the list, which will cause focus to shift
+ //to the topright cell. Reset focus onto the clicked button's cell.
+ ((sender as Control).Parent.Parent as DataGridCell)?.Focus();
+ }
+ else if (button.DataContext is LibraryBookEntry2 lbEntry)
+ {
+ LiberateClicked?.Invoke(this, lbEntry.LibraryBook);
+ }
+ }
+
+ public async void Cover_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
+ {
+ if (sender is not Image tblock || tblock.DataContext is not GridEntry2 gEntry)
+ return;
+
+ var picDefinition = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
+ var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
+
+ (_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(gEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
+ var windowTitle = $"{gEntry.Title} - Cover";
+
+ if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
+ {
+ imageDisplay = new GridView.ImageDisplay();
+ imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
+ imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
+ imageDisplay.Show(null);
+ }
+
+ imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(gEntry.LibraryBook);
+ imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(gEntry.LibraryBook, ".jpg"));
+ imageDisplay.Text = windowTitle;
+ imageDisplay.CoverPicture = initialImageBts;
+ imageDisplay.CoverPicture = await picDlTask;
+ }
+
+ public void Description_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
+ {
+ if (sender is TextBlock tblock && tblock.DataContext is GridEntry2 gEntry)
+ {
+ var pt = tblock.Parent.PointToScreen(tblock.Parent.Bounds.TopRight);
+ var displayWindow = new GridView.DescriptionDisplay
+ {
+ SpawnLocation = new System.Drawing.Point(pt.X, pt.Y),
+ DescriptionText = gEntry.LongDescription,
+ BorderThickness = 2,
+ };
+
+ void CloseWindow(object o, DataGridRowEventArgs e)
+ {
+ displayWindow.Close();
+ }
+ productsGrid.LoadingRow += CloseWindow;
+ displayWindow.FormClosed += (_, _) =>
+ {
+ productsGrid.LoadingRow -= CloseWindow;
+ };
+
+ displayWindow.Show();
+ }
+ }
+
+ public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
+ {
+ var button = args.Source as Button;
+
+ if (button.DataContext is LibraryBookEntry2 lbEntry)
+ {
+ var bookDetailsForm = new Dialogs.BookDetailsDialog(lbEntry.LibraryBook);
+ if (bookDetailsForm.ShowDialog() == System.Windows.Forms.DialogResult.OK)
+ lbEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
+ }
+ }
+ }
+}
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.ColumnCustomization.xaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.ColumnCustomization.xaml.cs
new file mode 100644
index 00000000..50fe68d2
--- /dev/null
+++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.ColumnCustomization.xaml.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
+{
+ public partial class ProductsDisplay2
+ {
+ private void Configure_ColumnCustomization() { }
+ }
+}
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Display.xaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Display.xaml.cs
new file mode 100644
index 00000000..5c934399
--- /dev/null
+++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Display.xaml.cs
@@ -0,0 +1,62 @@
+using ApplicationServices;
+using Avalonia.Controls;
+using LibationWinForms.AvaloniaUI.ViewModels;
+using System;
+using System.Collections;
+using System.Linq;
+using System.Reflection;
+
+namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
+{
+ public partial class ProductsDisplay2
+ {
+ private void Configure_Display() { }
+
+ public void Display()
+ {
+ try
+ {
+ var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
+
+ if (_viewModel is null)
+ {
+ _viewModel = new ProductsDisplayViewModel(dbBooks);
+ InitialLoaded?.Invoke(this, EventArgs.Empty);
+ VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
+
+ //Avalonia displays items in the DataConncetion from an internal copy of
+ //the bound list, not the actual bound list. so we need to reflect to get
+ //the current display order and set the GridEntry.ListIndex correctly.
+ var DataConnection_PI = typeof(DataGrid).GetProperty("DataConnection", BindingFlags.NonPublic | BindingFlags.Instance);
+ var DataSource_PI = DataConnection_PI.PropertyType.GetProperty("DataSource", BindingFlags.Public | BindingFlags.Instance);
+
+ bindingList.CollectionChanged += (s, e) =>
+ {
+ var displayListGE = ((IEnumerable)DataSource_PI.GetValue(DataConnection_PI.GetValue(productsGrid))).Cast();
+ int index = 0;
+ foreach (var di in displayListGE)
+ {
+ di.ListIndex = index++;
+ }
+ };
+
+ //Assign the viewmodel after we subscribe to CollectionChanged
+ //to ensure that out handler executes first.
+ productsGrid.DataContext = _viewModel;
+ }
+ else
+ {
+ string existingFilter = _viewModel?.GridEntries?.Filter;
+ bindingList.ReplaceList(ProductsDisplayViewModel.CreateGridEntries(dbBooks));
+ bindingList.Filter = existingFilter;
+ ReSort();
+ }
+
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay2));
+ }
+ }
+ }
+}
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Filtering.xaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Filtering.xaml.cs
new file mode 100644
index 00000000..17388697
--- /dev/null
+++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Filtering.xaml.cs
@@ -0,0 +1,27 @@
+using LibationWinForms.AvaloniaUI.ViewModels;
+using System;
+using System.Linq;
+
+namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
+{
+ public partial class ProductsDisplay2
+ {
+ private void Configure_Filtering() { }
+
+ public void Filter(string searchString)
+ {
+ int visibleCount = bindingList.Count;
+
+ if (string.IsNullOrEmpty(searchString))
+ bindingList.RemoveFilter();
+ else
+ bindingList.Filter = searchString;
+
+ if (visibleCount != bindingList.Count)
+ VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
+
+ //Re-sort after filtering
+ ReSort();
+ }
+ }
+}
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.ScanAndRemove.xaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.ScanAndRemove.xaml.cs
new file mode 100644
index 00000000..025ed866
--- /dev/null
+++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.ScanAndRemove.xaml.cs
@@ -0,0 +1,106 @@
+using ApplicationServices;
+using AudibleUtilities;
+using DataLayer;
+using LibationWinForms.AvaloniaUI.ViewModels;
+using ReactiveUI;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
+{
+ public partial class ProductsDisplay2
+ {
+ private void Configure_ScanAndRemove() { }
+
+ public void CloseRemoveBooksColumn()
+ => removeGVColumn.IsVisible = false;
+
+ public async Task RemoveCheckedBooksAsync()
+ {
+ var selectedBooks = GetAllBookEntries().Where(lbe => lbe.Remove == true).ToList();
+
+ if (selectedBooks.Count == 0)
+ return;
+
+ var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
+ var result = MessageBoxLib.ShowConfirmationDialog(
+ libraryBooks,
+ $"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
+ "Remove books from Libation?");
+
+ if (result != System.Windows.Forms.DialogResult.Yes)
+ return;
+
+ RemoveBooks(selectedBooks);
+ var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
+ var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
+
+ RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
+ }
+ public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
+ {
+ RemovableCountChanged?.Invoke(this, 0);
+ removeGVColumn.IsVisible = true;
+
+ try
+ {
+ if (accounts is null || accounts.Length == 0)
+ return;
+
+ var allBooks = GetAllBookEntries();
+
+ foreach (var b in allBooks)
+ b.Remove = false;
+
+ var lib = allBooks
+ .Select(lbe => lbe.LibraryBook)
+ .Where(lb => !lb.Book.HasLiberated());
+
+ var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts);
+
+ var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
+
+ foreach (var r in removable)
+ r.Remove = true;
+
+ RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
+ }
+ catch (Exception ex)
+ {
+ MessageBoxLib.ShowAdminAlert(
+ null,
+ "Error scanning library. You may still manually select books to remove from Libation's library.",
+ "Error scanning library",
+ ex);
+ }
+ }
+
+ private void RemoveBooks(IEnumerable removedBooks)
+ {
+ //Remove books in series from their parents' Children list
+ foreach (var removed in removedBooks.Where(b => b.Parent is not null))
+ {
+ removed.Parent.Children.Remove(removed);
+
+ //In Avalonia, if you fire PropertyChanged with an empty or invalid property name, nothing is updated.
+ //So we must notify for specific properties that we believed changed.
+ removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.Length));
+ removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.PurchaseDate));
+ }
+
+ //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.BookEntries().Count());
+ }
+ }
+}
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Sorting.xaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Sorting.xaml.cs
new file mode 100644
index 00000000..9e4fe5b5
--- /dev/null
+++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Sorting.xaml.cs
@@ -0,0 +1,65 @@
+using Avalonia.Controls;
+using LibationWinForms.AvaloniaUI.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
+{
+ public partial class ProductsDisplay2
+ {
+ private void Configure_Sorting() { }
+
+
+ private void RegisterCustomColumnComparers()
+ {
+
+ removeGVColumn.CustomSortComparer = new RowComparer(removeGVColumn);
+ liberateGVColumn.CustomSortComparer = new RowComparer(liberateGVColumn);
+ titleGVColumn.CustomSortComparer = new RowComparer(titleGVColumn);
+ authorsGVColumn.CustomSortComparer = new RowComparer(authorsGVColumn);
+ narratorsGVColumn.CustomSortComparer = new RowComparer(narratorsGVColumn);
+ lengthGVColumn.CustomSortComparer = new RowComparer(lengthGVColumn);
+ seriesGVColumn.CustomSortComparer = new RowComparer(seriesGVColumn);
+ descriptionGVColumn.CustomSortComparer = new RowComparer(descriptionGVColumn);
+ categoryGVColumn.CustomSortComparer = new RowComparer(categoryGVColumn);
+ productRatingGVColumn.CustomSortComparer = new RowComparer(productRatingGVColumn);
+ purchaseDateGVColumn.CustomSortComparer = new RowComparer(purchaseDateGVColumn);
+ myRatingGVColumn.CustomSortComparer = new RowComparer(myRatingGVColumn);
+ miscGVColumn.CustomSortComparer = new RowComparer(miscGVColumn);
+ tagAndDetailsGVColumn.CustomSortComparer = new RowComparer(tagAndDetailsGVColumn);
+ }
+
+ private void ReSort()
+ {
+ if (CurrentSortColumn is null)
+ {
+ bindingList.InternalList.Sort((i1, i2) => i2.DateAdded.CompareTo(i1.DateAdded));
+ bindingList.ResetCollection();
+ }
+ else
+ {
+ CurrentSortColumn.Sort(((RowComparer)CurrentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
+ }
+ }
+
+
+
+ private DataGridColumn CurrentSortColumn;
+
+
+ private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
+ {
+ //Force the comparer to get the current sort order. We can't
+ //retrieve it from inside this event handler because Avalonia
+ //doesn't set the property until after this event.
+ var comparer = e.Column.CustomSortComparer as RowComparer;
+ comparer.SortDirection = null;
+
+ CurrentSortColumn = e.Column;
+ }
+ }
+}
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.axaml
similarity index 96%
rename from Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml
rename to Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.axaml
index 50361033..82c68edd 100644
--- a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml
+++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.axaml
@@ -5,11 +5,12 @@
xmlns:views="clr-namespace:LibationWinForms.AvaloniaUI.Views"
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="1560" d:DesignHeight="400"
- x:Class="LibationWinForms.AvaloniaUI.Views.ProductsDisplay2">
+ x:Class="LibationWinForms.AvaloniaUI.Views.ProductsGrid.ProductsDisplay2">
-
-
-
+
+
+
+
@@ -147,7 +148,7 @@
-
+
diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.axaml.cs
new file mode 100644
index 00000000..96452776
--- /dev/null
+++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.axaml.cs
@@ -0,0 +1,121 @@
+using ApplicationServices;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using DataLayer;
+using LibationWinForms.AvaloniaUI.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
+{
+ public partial class ProductsDisplay2 : UserControl
+ {
+ /// Number of visible rows has changed
+ public event EventHandler VisibleCountChanged;
+ public event EventHandler RemovableCountChanged;
+ public event EventHandler LiberateClicked;
+ public event EventHandler InitialLoaded;
+
+
+ public List GetVisibleBookEntries()
+ => bindingList
+ .BookEntries()
+ .Select(lbe => lbe.LibraryBook)
+ .ToList();
+ private IEnumerable GetAllBookEntries()
+ => bindingList
+ .AllItems()
+ .BookEntries();
+
+ private ProductsDisplayViewModel _viewModel;
+ private GridEntryBindingList2 bindingList => _viewModel.GridEntries;
+
+ DataGridColumn removeGVColumn;
+ DataGridColumn liberateGVColumn;
+ DataGridColumn coverGVColumn;
+ DataGridColumn titleGVColumn;
+ DataGridColumn authorsGVColumn;
+ DataGridColumn narratorsGVColumn;
+ DataGridColumn lengthGVColumn;
+ DataGridColumn seriesGVColumn;
+ DataGridColumn descriptionGVColumn;
+ DataGridColumn categoryGVColumn;
+ DataGridColumn productRatingGVColumn;
+ DataGridColumn purchaseDateGVColumn;
+ DataGridColumn myRatingGVColumn;
+ DataGridColumn miscGVColumn;
+ DataGridColumn tagAndDetailsGVColumn;
+
+ public ProductsDisplay2()
+ {
+ InitializeComponent();
+
+ Configure_Buttons();
+ Configure_ColumnCustomization();
+ Configure_Display();
+ Configure_Filtering();
+ Configure_ScanAndRemove();
+ Configure_Sorting();
+
+ if (Design.IsDesignMode)
+ {
+ using var context = DbContexts.GetContext();
+ var book = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
+ productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(new List { book });
+ return;
+ }
+
+ }
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ productsGrid = this.FindControl(nameof(productsGrid));
+ productsGrid.Sorting += ProductsGrid_Sorting;
+ productsGrid.CanUserSortColumns = true;
+ productsGrid.LoadingRow += ProductsGrid_LoadingRow;
+
+ removeGVColumn = productsGrid.Columns[0];
+ liberateGVColumn = productsGrid.Columns[1];
+ coverGVColumn = productsGrid.Columns[2];
+ titleGVColumn = productsGrid.Columns[3];
+ authorsGVColumn = productsGrid.Columns[4];
+ narratorsGVColumn = productsGrid.Columns[5];
+ lengthGVColumn = productsGrid.Columns[6];
+ seriesGVColumn = productsGrid.Columns[7];
+ descriptionGVColumn = productsGrid.Columns[8];
+ categoryGVColumn = productsGrid.Columns[9];
+ productRatingGVColumn = productsGrid.Columns[10];
+ purchaseDateGVColumn = productsGrid.Columns[11];
+ myRatingGVColumn = productsGrid.Columns[12];
+ miscGVColumn = productsGrid.Columns[13];
+ tagAndDetailsGVColumn = productsGrid.Columns[14];
+
+ RegisterCustomColumnComparers();
+ }
+
+ private static object tagObj = new();
+ private void ProductsGrid_LoadingRow(object sender, DataGridRowEventArgs e)
+ {
+ if (e.Row.Tag == tagObj)
+ return;
+ e.Row.Tag = tagObj;
+
+ static IBrush GetRowColor(DataGridRow row)
+ => row.DataContext is GridEntry2 gEntry
+ && gEntry is LibraryBookEntry2 lbEntry
+ && lbEntry.Parent is not null
+ ? App.SeriesEntryGridBackgroundBrush
+ : null;
+
+ e.Row.Background = GetRowColor(e.Row);
+ e.Row.DataContextChanged += (sender, e) =>
+ {
+ var row = sender as DataGridRow;
+ row.Background = GetRowColor(row);
+ };
+ }
+ }
+}