From 920f4df213c0b3a11deda3fff9d93277f652bf54 Mon Sep 17 00:00:00 2001
From: Michael Bucari-Tovo
Date: Tue, 7 Jun 2022 15:28:16 -0600
Subject: [PATCH] Use new ContentType.Parent to add series info to grid display
---
Source/LibationWinForms/GridView/GridEntry.cs | 120 +++++++----
.../GridView/GridEntryBindingList.cs | 14 +-
.../GridView/LibraryBookEntry.cs | 89 +--------
.../GridView/ProductsDisplay.Designer.cs | 8 +-
.../GridView/ProductsDisplay.cs | 5 +-
.../LibationWinForms/GridView/ProductsGrid.cs | 188 +++++++++++-------
.../GridView/QueryExtensions.cs | 58 ++++++
.../LibationWinForms/GridView/SeriesEntry.cs | 106 ++++------
8 files changed, 313 insertions(+), 275 deletions(-)
create mode 100644 Source/LibationWinForms/GridView/QueryExtensions.cs
diff --git a/Source/LibationWinForms/GridView/GridEntry.cs b/Source/LibationWinForms/GridView/GridEntry.cs
index 4b1a614c..b2e2ee21 100644
--- a/Source/LibationWinForms/GridView/GridEntry.cs
+++ b/Source/LibationWinForms/GridView/GridEntry.cs
@@ -1,4 +1,5 @@
using DataLayer;
+using Dinah.Core;
using Dinah.Core.DataBinding;
using Dinah.Core.Drawing;
using LibationFileManager;
@@ -10,11 +11,14 @@ using System.Linq;
namespace LibationWinForms.GridView
{
+ /// The View Model base for the DataGridView
public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
{
- protected abstract Book Book { get; }
-
+ public string AudibleProductId => Book.AudibleProductId;
+ public LibraryBook LibraryBook { get; protected set; }
+ protected Book Book => LibraryBook.Book;
private Image _cover;
+
#region Model properties exposed to the view
public Image Cover
{
@@ -25,20 +29,20 @@ namespace LibationWinForms.GridView
NotifyPropertyChanged();
}
}
- public new bool InvokeRequired => base.InvokeRequired;
+ public float SeriesIndex { get; protected set; }
+ public string ProductRating { get; protected set; }
+ public string PurchaseDate { get; protected set; }
+ public string MyRating { 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 LongDescription { get; protected set; }
public abstract DateTime DateAdded { get; }
- public abstract float SeriesIndex { get; }
- public abstract string ProductRating { get; protected set; }
- public abstract string PurchaseDate { get; protected set; }
- public abstract string MyRating { get; protected set; }
- public abstract string Series { get; protected set; }
- public abstract string Title { get; protected set; }
- public abstract string Length { get; protected set; }
- public abstract string Authors { get; protected set; }
- public abstract string Narrators { get; protected set; }
- public abstract string Category { get; protected set; }
- public abstract string Misc { get; protected set; }
- public abstract string Description { get; protected set; }
public abstract string DisplayTags { get; }
public abstract LiberateButtonStatus Liberate { get; }
#endregion
@@ -54,6 +58,17 @@ namespace LibationWinForms.GridView
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
+ // Instantiate comparers for every exposed member object type.
+ private static readonly Dictionary _memberTypeComparers = new()
+ {
+ { typeof(string), new ObjectComparer() },
+ { typeof(int), new ObjectComparer() },
+ { typeof(float), new ObjectComparer() },
+ { typeof(bool), new ObjectComparer() },
+ { typeof(DateTime), new ObjectComparer() },
+ { typeof(LiberateButtonStatus), new ObjectComparer() },
+ };
+
#endregion
protected void LoadCover()
@@ -79,36 +94,61 @@ namespace LibationWinForms.GridView
}
}
- // Instantiate comparers for every exposed member object type.
- private static readonly Dictionary _memberTypeComparers = new()
+ #region Static library display functions
+
+ ///
+ /// This information should not change during lifetime, so call only once.
+ ///
+ protected static string GetDescriptionDisplay(Book book)
{
- { typeof(string), new ObjectComparer() },
- { typeof(int), new ObjectComparer() },
- { typeof(float), new ObjectComparer() },
- { typeof(bool), new ObjectComparer() },
- { typeof(DateTime), new ObjectComparer() },
- { typeof(LiberateButtonStatus), new ObjectComparer() },
- };
+ var doc = new HtmlAgilityPack.HtmlDocument();
+ doc.LoadHtml(book?.Description?.Replace("
", "\r\n\r\n") ?? "");
+ return doc.DocumentNode.InnerText.Trim();
+ }
+
+ protected static string TrimTextToWord(string text, int maxLength)
+ {
+ return
+ text.Length <= maxLength ?
+ text :
+ text.Substring(0, maxLength - 3) + "...";
+ }
+
+
+ ///
+ /// This information should not change during lifetime, so call only once.
+ /// Maximum of 5 text rows will fit in 80-pixel row height.
+ ///
+ protected static string GetMiscDisplay(LibraryBook libraryBook)
+ {
+ var details = new List();
+
+ var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]");
+ var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]");
+
+ details.Add($"Account: {locale} - {acct}");
+
+ if (libraryBook.Book.HasPdf())
+ details.Add("Has PDF");
+ if (libraryBook.Book.IsAbridged)
+ details.Add("Abridged");
+ if (libraryBook.Book.DatePublished.HasValue)
+ details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}");
+ // this goes last since it's most likely to have a line-break
+ if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher))
+ details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}");
+
+ if (!details.Any())
+ return "[details not imported]";
+
+ return string.Join("\r\n", details);
+ }
+
+ #endregion
~GridEntry()
{
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
-
- internal static class GridEntryExtensions
- {
-#nullable enable
- public static IEnumerable Series(this IEnumerable gridEntries)
- => gridEntries.OfType();
- public static IEnumerable LibraryBooks(this IEnumerable gridEntries)
- => gridEntries.OfType();
- public static LibraryBookEntry? FindBookByAsin(this IEnumerable gridEntries, string audibleProductID)
- => gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
- public static SeriesEntry? FindBookSeriesEntry(this IEnumerable gridEntries, IEnumerable matchSeries)
- => gridEntries.Series().FirstOrDefault(i => matchSeries.Any(s => s.Series.Name == i.Series));
- public static IEnumerable EmptySeries(this IEnumerable gridEntries)
- => gridEntries.Series().Where(i => i.Children.Count == 0);
- public static bool IsEpisodeChild(this LibraryBook lb) => lb.Book.ContentType == ContentType.Episode;
- }
}
diff --git a/Source/LibationWinForms/GridView/GridEntryBindingList.cs b/Source/LibationWinForms/GridView/GridEntryBindingList.cs
index 58be850c..e2b45c61 100644
--- a/Source/LibationWinForms/GridView/GridEntryBindingList.cs
+++ b/Source/LibationWinForms/GridView/GridEntryBindingList.cs
@@ -73,10 +73,10 @@ namespace LibationWinForms.GridView
FilterString = filterString;
SearchResults = SearchEngineCommands.Search(filterString);
- var booksFilteredIn = Items.LibraryBooks().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
+ var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
//Find all series containing children that match the search criteria
- var seriesFilteredIn = Items.Series().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
+ var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
@@ -89,19 +89,19 @@ namespace LibationWinForms.GridView
public void CollapseAll()
{
- foreach (var series in Items.Series().ToList())
+ foreach (var series in Items.SeriesEntries().ToList())
CollapseItem(series);
}
public void ExpandAll()
{
- foreach (var series in Items.Series().ToList())
+ foreach (var series in Items.SeriesEntries().ToList())
ExpandItem(series);
}
public void CollapseItem(SeriesEntry sEntry)
{
- foreach (var episode in Items.LibraryBooks().Where(b => b.Parent == sEntry).ToList())
+ foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).ToList())
{
FilterRemoved.Add(episode);
base.Remove(episode);
@@ -114,7 +114,7 @@ namespace LibationWinForms.GridView
{
var sindex = Items.IndexOf(sEntry);
- foreach (var episode in FilterRemoved.LibraryBooks().Where(b => b.Parent == sEntry).ToList())
+ foreach (var episode in FilterRemoved.BookEntries().Where(b => b.Parent == sEntry).ToList())
{
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
{
@@ -174,7 +174,7 @@ namespace LibationWinForms.GridView
{
var itemsList = (List)Items;
- var children = itemsList.LibraryBooks().Where(i => i.Parent is not null).ToList();
+ var children = itemsList.BookEntries().Where(i => i.Parent is not null).ToList();
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
diff --git a/Source/LibationWinForms/GridView/LibraryBookEntry.cs b/Source/LibationWinForms/GridView/LibraryBookEntry.cs
index be6836a8..cddde1f5 100644
--- a/Source/LibationWinForms/GridView/LibraryBookEntry.cs
+++ b/Source/LibationWinForms/GridView/LibraryBookEntry.cs
@@ -3,29 +3,13 @@ using DataLayer;
using Dinah.Core;
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Linq;
namespace LibationWinForms.GridView
{
- ///
- /// The View Model for a LibraryBook
- ///
+ /// The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode
public class LibraryBookEntry : GridEntry
{
- #region implementation properties NOT exposed to the view
- // hide from public fields from Data Source GUI with [Browsable(false)]
-
- [Browsable(false)]
- public string AudibleProductId => Book.AudibleProductId;
- [Browsable(false)]
- public LibraryBook LibraryBook { get; private set; }
- [Browsable(false)]
- public string LongDescription { get; private set; }
- #endregion
-
- protected override Book Book => LibraryBook.Book;
-
#region Model properties exposed to the view
private DateTime lastStatusUpdate = default;
@@ -33,21 +17,8 @@ namespace LibationWinForms.GridView
private LiberatedStatus? _pdfStatus;
public override DateTime DateAdded => LibraryBook.DateAdded;
- public override float SeriesIndex => Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
- public override string ProductRating { get; protected set; }
- public override string PurchaseDate { get; protected set; }
- public override string MyRating { get; protected set; }
- public override string Series { get; protected set; }
- public override string Title { get; protected set; }
- public override string Length { get; protected set; }
- public override string Authors { get; protected set; }
- public override string Narrators { get; protected set; }
- public override string Category { get; protected set; }
- public override string Misc { get; protected set; }
- public override string Description { get; protected set; }
public override string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
- // these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint
public override LiberateButtonStatus Liberate
{
get
@@ -62,15 +33,17 @@ namespace LibationWinForms.GridView
return new LiberateButtonStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
}
}
+
#endregion
+ public SeriesEntry Parent { get; init; }
+
public LibraryBookEntry(LibraryBook libraryBook)
{
setLibraryBook(libraryBook);
LoadCover();
}
- public SeriesEntry Parent { get; init; }
public void UpdateLibraryBook(LibraryBook libraryBook)
{
if (AudibleProductId != libraryBook.Book.AudibleProductId)
@@ -100,12 +73,12 @@ namespace LibationWinForms.GridView
Misc = GetMiscDisplay(libraryBook);
LongDescription = GetDescriptionDisplay(Book);
Description = TrimTextToWord(LongDescription, 62);
+ SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
}
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
-
#region detect changes to the model, update the view, and save to database.
///
@@ -169,58 +142,6 @@ namespace LibationWinForms.GridView
{ nameof(DateAdded), () => DateAdded },
};
-
- #endregion
-
- #region Static library display functions
-
- ///
- /// This information should not change during lifetime, so call only once.
- ///
- private static string GetDescriptionDisplay(Book book)
- {
- var doc = new HtmlAgilityPack.HtmlDocument();
- doc.LoadHtml(book?.Description?.Replace(" ", "\r\n\r\n") ?? "");
- return doc.DocumentNode.InnerText.Trim();
- }
-
- private static string TrimTextToWord(string text, int maxLength)
- {
- return
- text.Length <= maxLength ?
- text :
- text.Substring(0, maxLength - 3) + "...";
- }
-
- ///
- /// This information should not change during lifetime, so call only once.
- /// Maximum of 5 text rows will fit in 80-pixel row height.
- ///
- private static string GetMiscDisplay(LibraryBook libraryBook)
- {
- var details = new List();
-
- var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]");
- var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]");
-
- details.Add($"Account: {locale} - {acct}");
-
- if (libraryBook.Book.HasPdf())
- details.Add("Has PDF");
- if (libraryBook.Book.IsAbridged)
- details.Add("Abridged");
- if (libraryBook.Book.DatePublished.HasValue)
- details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}");
- // this goes last since it's most likely to have a line-break
- if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher))
- details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}");
-
- if (!details.Any())
- return "[details not imported]";
-
- return string.Join("\r\n", details);
- }
-
#endregion
~LibraryBookEntry()
diff --git a/Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs b/Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs
index c4b1a543..db9ca6a5 100644
--- a/Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs
+++ b/Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs
@@ -39,10 +39,10 @@
this.productsGrid.Name = "productsGrid";
this.productsGrid.Size = new System.Drawing.Size(1510, 380);
this.productsGrid.TabIndex = 0;
- this.productsGrid.LiberateClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
- this.productsGrid.CoverClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_CoverClicked);
- this.productsGrid.DetailsClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
- this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
+ this.productsGrid.LiberateClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
+ this.productsGrid.CoverClicked += new LibationWinForms.GridView.GridEntryClickedEventHandler(this.productsGrid_CoverClicked);
+ this.productsGrid.DetailsClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
+ this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.GridEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
this.productsGrid.VisibleCountChanged += new System.EventHandler(this.productsGrid_VisibleCountChanged);
//
// ProductsDisplay
diff --git a/Source/LibationWinForms/GridView/ProductsDisplay.cs b/Source/LibationWinForms/GridView/ProductsDisplay.cs
index 7c411f3f..5f91a581 100644
--- a/Source/LibationWinForms/GridView/ProductsDisplay.cs
+++ b/Source/LibationWinForms/GridView/ProductsDisplay.cs
@@ -29,7 +29,7 @@ namespace LibationWinForms.GridView
#region Button controls
private ImageDisplay imageDisplay;
- private async void productsGrid_CoverClicked(LibraryBookEntry liveGridEntry)
+ private async void productsGrid_CoverClicked(GridEntry liveGridEntry)
{
var picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
@@ -52,7 +52,7 @@ namespace LibationWinForms.GridView
imageDisplay.CoverPicture = await picDlTask;
}
- private void productsGrid_DescriptionClicked(LibraryBookEntry liveGridEntry, Rectangle cellRectangle)
+ private void productsGrid_DescriptionClicked(GridEntry liveGridEntry, Rectangle cellRectangle)
{
var displayWindow = new DescriptionDisplay
{
@@ -103,7 +103,6 @@ namespace LibationWinForms.GridView
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay));
}
-
}
#endregion
diff --git a/Source/LibationWinForms/GridView/ProductsGrid.cs b/Source/LibationWinForms/GridView/ProductsGrid.cs
index 24dafc73..b762a046 100644
--- a/Source/LibationWinForms/GridView/ProductsGrid.cs
+++ b/Source/LibationWinForms/GridView/ProductsGrid.cs
@@ -10,23 +10,24 @@ using System.Windows.Forms;
namespace LibationWinForms.GridView
{
+ public delegate void GridEntryClickedEventHandler(GridEntry liveGridEntry);
+ public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry);
+ public delegate void GridEntryRectangleClickedEventHandler(GridEntry liveGridEntry, Rectangle cellRectangle);
+
public partial class ProductsGrid : UserControl
{
- public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry);
- public delegate void LibraryBookEntryRectangleClickedEventHandler(LibraryBookEntry liveGridEntry, Rectangle cellRectangle);
-
/// Number of visible rows has changed
public event EventHandler VisibleCountChanged;
public event LibraryBookEntryClickedEventHandler LiberateClicked;
- public event LibraryBookEntryClickedEventHandler CoverClicked;
+ public event GridEntryClickedEventHandler CoverClicked;
public event LibraryBookEntryClickedEventHandler DetailsClicked;
- public event LibraryBookEntryRectangleClickedEventHandler DescriptionClicked;
+ public event GridEntryRectangleClickedEventHandler DescriptionClicked;
public new event EventHandler Scroll;
private GridEntryBindingList bindingList;
internal IEnumerable GetVisible()
=> bindingList
- .LibraryBooks();
+ .BookEntries();
public ProductsGrid()
{
@@ -61,16 +62,23 @@ namespace LibationWinForms.GridView
else if (e.ColumnIndex == coverGVColumn.Index)
CoverClicked?.Invoke(lbEntry);
}
- else if (entry is SeriesEntry sEntry && e.ColumnIndex == liberateGVColumn.Index)
+ else if (entry is SeriesEntry sEntry)
{
- if (sEntry.Liberate.Expanded)
- bindingList.CollapseItem(sEntry);
- else
- bindingList.ExpandItem(sEntry);
+ if (e.ColumnIndex == liberateGVColumn.Index)
+ {
+ if (sEntry.Liberate.Expanded)
+ bindingList.CollapseItem(sEntry);
+ else
+ bindingList.ExpandItem(sEntry);
- sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate));
+ sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate));
- VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
+ VisibleCountChanged?.Invoke(this, bindingList.BookEntries().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);
}
}
@@ -82,14 +90,18 @@ namespace LibationWinForms.GridView
internal void BindToGrid(List dbBooks)
{
- var geList = dbBooks.Where(b => b.Book.ContentType is not ContentType.Episode).Select(b => new LibraryBookEntry(b)).Cast().ToList();
+ var geList = dbBooks.Where(lb => lb.IsProduct()).Select(b => new LibraryBookEntry(b)).Cast().ToList();
- var episodes = dbBooks.Where(b => b.IsEpisodeChild()).ToList();
-
- var allSeries = episodes.SelectMany(lb => lb.Book.SeriesLink.Where(s => !s.Series.AudibleSeriesId.StartsWith("SERIES_"))).DistinctBy(s => s.Series).ToList();
- foreach (var series in allSeries)
+ var parents = dbBooks.Where(lb => lb.IsEpisodeParent());
+ var episodes = dbBooks.Where(lb => lb.IsEpisodeChild());
+
+ foreach (var parent in parents)
{
- var seriesEntry = new SeriesEntry(series, episodes.Where(lb => lb.Book.SeriesLink.Any(s => s.Series == series.Series)));
+ var seriesEpisodes = episodes.Where(lb => lb.Book.SeriesLink?.Any(s => s.Series.AudibleSeriesId == parent.Book.AudibleProductId) == true).ToList();
+
+ if (!seriesEpisodes.Any()) continue;
+
+ var seriesEntry = new SeriesEntry(parent, seriesEpisodes);
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
@@ -98,79 +110,47 @@ namespace LibationWinForms.GridView
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
bindingList.CollapseAll();
syncBindingSource.DataSource = bindingList;
- VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
- }
+ VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
+ }
internal void UpdateGrid(List dbBooks)
{
+ #region Add new or update existing grid entries
+
+ //Remove filter prior to adding/updating boooks
string existingFilter = syncBindingSource.Filter;
Filter(null);
bindingList.SuspendFilteringOnUpdate = true;
- //Add absent books to grid, or update current books
+ //Add absent entries to grid, or update existing entry
- var allItmes = bindingList.AllItems().LibraryBooks();
- foreach (var libraryBook in dbBooks)
+ var allEntries = bindingList.AllItems().BookEntries();
+ var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
+
+ foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
{
- var existingItem = allItmes.FindBookByAsin(libraryBook.Book.AudibleProductId);
+ var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
- // add new to top
- if (existingItem is null)
- {
- if (libraryBook.IsEpisodeChild())
- {
- LibraryBookEntry lbe;
- //Find the series that libraryBook belongs to, if it exists
- var series = bindingList.AllItems().FindBookSeriesEntry(libraryBook.Book.SeriesLink);
-
- if (series is null)
- {
- //Series doesn't exist yet, so create and add it
- var newSeries = new SeriesEntry(libraryBook.Book.SeriesLink.First(), libraryBook);
- lbe = newSeries.Children[0];
- newSeries.Liberate.Expanded = true;
- bindingList.Insert(0, newSeries);
- series = newSeries;
- }
- else
- {
- lbe = new(libraryBook) { Parent = series };
- series.Children.Add(lbe);
- }
- //Add episode beneath the parent
- int seriesIndex = bindingList.IndexOf(series);
- bindingList.Insert(seriesIndex + 1, lbe);
-
- if (series.Liberate.Expanded)
- bindingList.ExpandItem(series);
- else
- bindingList.CollapseItem(series);
-
- series.NotifyPropertyChanged();
- }
- else if (libraryBook.Book.ContentType is not ContentType.Episode)
- //Add the new product
- bindingList.Insert(0, new LibraryBookEntry(libraryBook));
- }
- // update existing
- else
- {
- existingItem.UpdateLibraryBook(libraryBook);
- }
- }
+ if (libraryBook.IsEpisodeChild())
+ AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
+ else if (libraryBook.IsProduct())
+ AddOrUpdateBook(libraryBook, existingEntry);
+ }
bindingList.SuspendFilteringOnUpdate = false;
- //Re-filter after updating existing / adding new books to capture any changes
+ //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()
- .LibraryBooks()
+ .BookEntries()
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
//Remove books in series from their parents' Children list
@@ -190,7 +170,69 @@ namespace LibationWinForms.GridView
//no need to re-filter for removed books
bindingList.Remove(removed);
- VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
+ VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
+ }
+
+ private void AddOrUpdateBook(LibraryBook book, LibraryBookEntry 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, LibraryBookEntry existingEpisodeEntry, List seriesEntries, IEnumerable dbBooks)
+ {
+ if (existingEpisodeEntry is null)
+ {
+ LibraryBookEntry 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)
+ {
+ var ex = new ApplicationException($"Episode's series parent not found in database.");
+ var seriesLinks = string.Join("\r\n", episodeBook.Book.SeriesLink?.Select(sb => $"{nameof(sb.Series.Name)}={sb.Series.Name}, {nameof(sb.Series.AudibleSeriesId)}={sb.Series.AudibleSeriesId}"));
+ Serilog.Log.Logger.Error(ex, "Episode={episodeBook}, Series: {seriesLinks}", episodeBook, seriesLinks);
+ throw ex;
+ }
+
+ 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(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.NotifyPropertyChanged();
+
+ }
+ else
+ existingEpisodeEntry.UpdateLibraryBook(episodeBook);
}
#endregion
@@ -207,7 +249,7 @@ namespace LibationWinForms.GridView
syncBindingSource.Filter = searchString;
if (visibleCount != bindingList.Count)
- VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
+ VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
diff --git a/Source/LibationWinForms/GridView/QueryExtensions.cs b/Source/LibationWinForms/GridView/QueryExtensions.cs
new file mode 100644
index 00000000..e6df05cb
--- /dev/null
+++ b/Source/LibationWinForms/GridView/QueryExtensions.cs
@@ -0,0 +1,58 @@
+using DataLayer;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace LibationWinForms.GridView
+{
+#nullable enable
+ internal static class QueryExtensions
+ {
+ public static IEnumerable BookEntries(this IEnumerable gridEntries)
+ => gridEntries.OfType();
+
+ public static IEnumerable SeriesEntries(this IEnumerable gridEntries)
+ => gridEntries.OfType();
+
+ public static T? FindByAsin(this IEnumerable gridEntries, string audibleProductID) where T : GridEntry
+ => gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
+
+ public static IEnumerable EmptySeries(this IEnumerable gridEntries)
+ => gridEntries.SeriesEntries().Where(i => i.Children.Count == 0);
+
+ public static bool IsProduct(this LibraryBook lb)
+ => lb.Book.ContentType is not ContentType.Episode and not ContentType.Parent;
+
+ public static bool IsEpisodeChild(this LibraryBook lb)
+ => lb.Book.ContentType is ContentType.Episode;
+
+ public static bool IsEpisodeParent(this LibraryBook lb)
+ => lb.Book.ContentType is ContentType.Parent;
+
+ public static SeriesEntry? FindSeriesParent(this IEnumerable gridEntries, LibraryBook seriesEpisode)
+ {
+ if (seriesEpisode.Book.SeriesLink is null) return null;
+
+ //Parent books will always have exactly 1 SeriesBook due to how
+ //they are imported in ApiExtended.getChildEpisodesAsync()
+ return gridEntries.SeriesEntries().FirstOrDefault(
+ lb =>
+ seriesEpisode.Book.SeriesLink.Any(
+ s => s.Series.AudibleSeriesId == lb.LibraryBook.Book.SeriesLink.Single().Series.AudibleSeriesId));
+ }
+
+ public static LibraryBook? FindSeriesParent(this IEnumerable libraryBooks, LibraryBook seriesEpisode)
+ {
+ if (seriesEpisode.Book.SeriesLink is null) return null;
+
+ //Parent books will always have exactly 1 SeriesBook due to how
+ //they are imported in ApiExtended.getChildEpisodesAsync()
+ return libraryBooks.FirstOrDefault(
+ lb =>
+ lb.IsEpisodeParent() &&
+ seriesEpisode.Book.SeriesLink.Any(
+ s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId));
+ }
+ }
+#nullable disable
+}
diff --git a/Source/LibationWinForms/GridView/SeriesEntry.cs b/Source/LibationWinForms/GridView/SeriesEntry.cs
index 96d40f49..781695f0 100644
--- a/Source/LibationWinForms/GridView/SeriesEntry.cs
+++ b/Source/LibationWinForms/GridView/SeriesEntry.cs
@@ -6,101 +6,79 @@ using System.Linq;
namespace LibationWinForms.GridView
{
+ /// The View Model for a LibraryBook that is ContentType.Parent
public class SeriesEntry : GridEntry
{
- public List Children { get; init; }
+ public List Children { get; } = new();
public override DateTime DateAdded => Children.Max(c => c.DateAdded);
- public override float SeriesIndex { get; }
- public override string ProductRating
- {
- get
- {
- var productAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.Rating.StoryRating));
- return productAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
- }
- protected set => throw new NotImplementedException();
- }
- public override string PurchaseDate { get; protected set; }
- public override string MyRating
- {
- get
- {
- var myAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.StoryRating));
- return myAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
- }
- protected set => throw new NotImplementedException();
- }
- public override string Series { get; protected set; }
- public override string Title { get; protected set; }
- public override string Length
- {
- get
- {
- int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
- return bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
- }
- protected set => throw new NotImplementedException();
- }
- public override string Authors { get; protected set; }
- public override string Narrators { get; protected set; }
- public override string Category { get; protected set; }
- public override string Misc { get; protected set; } = string.Empty;
- public override string Description { get; protected set; } = string.Empty;
public override string DisplayTags { get; } = string.Empty;
-
public override LiberateButtonStatus Liberate { get; }
- protected override Book Book => SeriesBook.Book;
-
- private SeriesBook SeriesBook { get; set; }
-
- private SeriesEntry(SeriesBook seriesBook)
+ private SeriesEntry(LibraryBook parent)
{
+ LibraryBook = parent;
Liberate = new LiberateButtonStatus { IsSeries = true };
- SeriesIndex = seriesBook.Index;
+ SeriesIndex = -1;
}
- public SeriesEntry(SeriesBook seriesBook, IEnumerable children) : this(seriesBook)
+
+ public SeriesEntry(LibraryBook parent, IEnumerable children) : this(parent)
{
- Children = children.Select(c => new LibraryBookEntry(c) { Parent = this }).OrderBy(c => c.SeriesIndex).ToList();
- SetSeriesBook(seriesBook);
+ Children = children
+ .Select(c => new LibraryBookEntry(c) { Parent = this })
+ .OrderBy(c => c.SeriesIndex)
+ .ToList();
+
+ UpdateSeries(parent);
+ LoadCover();
}
- public SeriesEntry(SeriesBook seriesBook, LibraryBook child) : this(seriesBook)
+
+ public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent)
{
Children = new() { new LibraryBookEntry(child) { Parent = this } };
- SetSeriesBook(seriesBook);
+
+ UpdateSeries(parent);
+ LoadCover();
}
- private void SetSeriesBook(SeriesBook seriesBook)
+ public void UpdateSeries(LibraryBook libraryBook)
{
- SeriesBook = seriesBook;
- LoadCover();
+ LibraryBook = libraryBook;
// Immutable properties
{
- Title = SeriesBook.Series.Name;
- Series = SeriesBook.Series.Name;
+ 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();
Category = string.Join(" > ", Book.CategoriesNames());
- }
- }
+ Misc = GetMiscDisplay(libraryBook);
+ LongDescription = GetDescriptionDisplay(Book);
+ Description = TrimTextToWord(LongDescription, 62);
+ int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
+ Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
+ }
+
+ NotifyPropertyChanged();
+ }
/// Create getters for all member object values by name
protected override Dictionary> CreateMemberValueDictionary() => new()
{
- { nameof(Title), () => Book.SeriesSortable() },
+ { nameof(Title), () => Book.TitleSortable() },
{ nameof(Series), () => Book.SeriesSortable() },
{ nameof(Length), () => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes) },
- { nameof(MyRating), () => Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.FirstScore()) },
+ { nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore() },
{ nameof(PurchaseDate), () => Children.Min(c => c.LibraryBook.DateAdded) },
- { nameof(ProductRating), () => Children.Average(c => c.LibraryBook.Book.Rating.FirstScore()) },
- { nameof(Authors), () => string.Empty },
- { nameof(Narrators), () => string.Empty },
- { nameof(Description), () => string.Empty },
- { nameof(Category), () => string.Empty },
- { nameof(Misc), () => string.Empty },
+ { nameof(ProductRating), () => Book.Rating.FirstScore() },
+ { nameof(Authors), () => Authors },
+ { nameof(Narrators), () => Narrators },
+ { nameof(Description), () => Description },
+ { nameof(Category), () => Category },
+ { nameof(Misc), () => Misc },
{ nameof(DisplayTags), () => string.Empty },
{ nameof(Liberate), () => Liberate },
{ nameof(DateAdded), () => DateAdded },