Use new ContentType.Parent to add series info to grid display
This commit is contained in:
parent
c48eacd9af
commit
920f4df213
@ -1,4 +1,5 @@
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
|
using Dinah.Core;
|
||||||
using Dinah.Core.DataBinding;
|
using Dinah.Core.DataBinding;
|
||||||
using Dinah.Core.Drawing;
|
using Dinah.Core.Drawing;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
@ -10,11 +11,14 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace LibationWinForms.GridView
|
namespace LibationWinForms.GridView
|
||||||
{
|
{
|
||||||
|
/// <summary>The View Model base for the DataGridView</summary>
|
||||||
public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
|
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;
|
private Image _cover;
|
||||||
|
|
||||||
#region Model properties exposed to the view
|
#region Model properties exposed to the view
|
||||||
public Image Cover
|
public Image Cover
|
||||||
{
|
{
|
||||||
@ -25,20 +29,20 @@ namespace LibationWinForms.GridView
|
|||||||
NotifyPropertyChanged();
|
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 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 string DisplayTags { get; }
|
||||||
public abstract LiberateButtonStatus Liberate { get; }
|
public abstract LiberateButtonStatus Liberate { get; }
|
||||||
#endregion
|
#endregion
|
||||||
@ -54,6 +58,17 @@ namespace LibationWinForms.GridView
|
|||||||
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
|
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
|
||||||
public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
|
public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
|
||||||
|
|
||||||
|
// Instantiate comparers for every exposed member object type.
|
||||||
|
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
|
||||||
|
{
|
||||||
|
{ typeof(string), new ObjectComparer<string>() },
|
||||||
|
{ typeof(int), new ObjectComparer<int>() },
|
||||||
|
{ typeof(float), new ObjectComparer<float>() },
|
||||||
|
{ typeof(bool), new ObjectComparer<bool>() },
|
||||||
|
{ typeof(DateTime), new ObjectComparer<DateTime>() },
|
||||||
|
{ typeof(LiberateButtonStatus), new ObjectComparer<LiberateButtonStatus>() },
|
||||||
|
};
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
protected void LoadCover()
|
protected void LoadCover()
|
||||||
@ -79,36 +94,61 @@ namespace LibationWinForms.GridView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instantiate comparers for every exposed member object type.
|
#region Static library display functions
|
||||||
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
|
|
||||||
|
/// <summary>
|
||||||
|
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
|
||||||
|
/// </summary>
|
||||||
|
protected static string GetDescriptionDisplay(Book book)
|
||||||
{
|
{
|
||||||
{ typeof(string), new ObjectComparer<string>() },
|
var doc = new HtmlAgilityPack.HtmlDocument();
|
||||||
{ typeof(int), new ObjectComparer<int>() },
|
doc.LoadHtml(book?.Description?.Replace("</p> ", "\r\n\r\n</p>") ?? "");
|
||||||
{ typeof(float), new ObjectComparer<float>() },
|
return doc.DocumentNode.InnerText.Trim();
|
||||||
{ typeof(bool), new ObjectComparer<bool>() },
|
}
|
||||||
{ typeof(DateTime), new ObjectComparer<DateTime>() },
|
|
||||||
{ typeof(LiberateButtonStatus), new ObjectComparer<LiberateButtonStatus>() },
|
protected static string TrimTextToWord(string text, int maxLength)
|
||||||
};
|
{
|
||||||
|
return
|
||||||
|
text.Length <= maxLength ?
|
||||||
|
text :
|
||||||
|
text.Substring(0, maxLength - 3) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
|
||||||
|
/// Maximum of 5 text rows will fit in 80-pixel row height.
|
||||||
|
/// </summary>
|
||||||
|
protected static string GetMiscDisplay(LibraryBook libraryBook)
|
||||||
|
{
|
||||||
|
var details = new List<string>();
|
||||||
|
|
||||||
|
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()
|
~GridEntry()
|
||||||
{
|
{
|
||||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class GridEntryExtensions
|
|
||||||
{
|
|
||||||
#nullable enable
|
|
||||||
public static IEnumerable<SeriesEntry> Series(this IEnumerable<GridEntry> gridEntries)
|
|
||||||
=> gridEntries.OfType<SeriesEntry>();
|
|
||||||
public static IEnumerable<LibraryBookEntry> LibraryBooks(this IEnumerable<GridEntry> gridEntries)
|
|
||||||
=> gridEntries.OfType<LibraryBookEntry>();
|
|
||||||
public static LibraryBookEntry? FindBookByAsin(this IEnumerable<LibraryBookEntry> gridEntries, string audibleProductID)
|
|
||||||
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
|
|
||||||
public static SeriesEntry? FindBookSeriesEntry(this IEnumerable<GridEntry> gridEntries, IEnumerable<SeriesBook> matchSeries)
|
|
||||||
=> gridEntries.Series().FirstOrDefault(i => matchSeries.Any(s => s.Series.Name == i.Series));
|
|
||||||
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> gridEntries)
|
|
||||||
=> gridEntries.Series().Where(i => i.Children.Count == 0);
|
|
||||||
public static bool IsEpisodeChild(this LibraryBook lb) => lb.Book.ContentType == ContentType.Episode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,10 +73,10 @@ namespace LibationWinForms.GridView
|
|||||||
FilterString = filterString;
|
FilterString = filterString;
|
||||||
SearchResults = SearchEngineCommands.Search(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
|
//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();
|
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
|
||||||
|
|
||||||
@ -89,19 +89,19 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
public void CollapseAll()
|
public void CollapseAll()
|
||||||
{
|
{
|
||||||
foreach (var series in Items.Series().ToList())
|
foreach (var series in Items.SeriesEntries().ToList())
|
||||||
CollapseItem(series);
|
CollapseItem(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExpandAll()
|
public void ExpandAll()
|
||||||
{
|
{
|
||||||
foreach (var series in Items.Series().ToList())
|
foreach (var series in Items.SeriesEntries().ToList())
|
||||||
ExpandItem(series);
|
ExpandItem(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CollapseItem(SeriesEntry sEntry)
|
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);
|
FilterRemoved.Add(episode);
|
||||||
base.Remove(episode);
|
base.Remove(episode);
|
||||||
@ -114,7 +114,7 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
var sindex = Items.IndexOf(sEntry);
|
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))
|
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
||||||
{
|
{
|
||||||
@ -174,7 +174,7 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
var itemsList = (List<GridEntry>)Items;
|
var itemsList = (List<GridEntry>)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();
|
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
|
||||||
|
|
||||||
|
|||||||
@ -3,29 +3,13 @@ using DataLayer;
|
|||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace LibationWinForms.GridView
|
namespace LibationWinForms.GridView
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode</summary>
|
||||||
/// The View Model for a LibraryBook
|
|
||||||
/// </summary>
|
|
||||||
public class LibraryBookEntry : GridEntry
|
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
|
#region Model properties exposed to the view
|
||||||
|
|
||||||
private DateTime lastStatusUpdate = default;
|
private DateTime lastStatusUpdate = default;
|
||||||
@ -33,21 +17,8 @@ namespace LibationWinForms.GridView
|
|||||||
private LiberatedStatus? _pdfStatus;
|
private LiberatedStatus? _pdfStatus;
|
||||||
|
|
||||||
public override DateTime DateAdded => LibraryBook.DateAdded;
|
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);
|
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
|
public override LiberateButtonStatus Liberate
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@ -62,15 +33,17 @@ namespace LibationWinForms.GridView
|
|||||||
return new LiberateButtonStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
|
return new LiberateButtonStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public SeriesEntry Parent { get; init; }
|
||||||
|
|
||||||
public LibraryBookEntry(LibraryBook libraryBook)
|
public LibraryBookEntry(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
setLibraryBook(libraryBook);
|
setLibraryBook(libraryBook);
|
||||||
LoadCover();
|
LoadCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SeriesEntry Parent { get; init; }
|
|
||||||
public void UpdateLibraryBook(LibraryBook libraryBook)
|
public void UpdateLibraryBook(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
if (AudibleProductId != libraryBook.Book.AudibleProductId)
|
if (AudibleProductId != libraryBook.Book.AudibleProductId)
|
||||||
@ -100,12 +73,12 @@ namespace LibationWinForms.GridView
|
|||||||
Misc = GetMiscDisplay(libraryBook);
|
Misc = GetMiscDisplay(libraryBook);
|
||||||
LongDescription = GetDescriptionDisplay(Book);
|
LongDescription = GetDescriptionDisplay(Book);
|
||||||
Description = TrimTextToWord(LongDescription, 62);
|
Description = TrimTextToWord(LongDescription, 62);
|
||||||
|
SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
|
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#region detect changes to the model, update the view, and save to database.
|
#region detect changes to the model, update the view, and save to database.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -169,58 +142,6 @@ namespace LibationWinForms.GridView
|
|||||||
{ nameof(DateAdded), () => DateAdded },
|
{ nameof(DateAdded), () => DateAdded },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static library display functions
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
|
|
||||||
/// </summary>
|
|
||||||
private static string GetDescriptionDisplay(Book book)
|
|
||||||
{
|
|
||||||
var doc = new HtmlAgilityPack.HtmlDocument();
|
|
||||||
doc.LoadHtml(book?.Description?.Replace("</p> ", "\r\n\r\n</p>") ?? "");
|
|
||||||
return doc.DocumentNode.InnerText.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string TrimTextToWord(string text, int maxLength)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
text.Length <= maxLength ?
|
|
||||||
text :
|
|
||||||
text.Substring(0, maxLength - 3) + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
|
|
||||||
/// Maximum of 5 text rows will fit in 80-pixel row height.
|
|
||||||
/// </summary>
|
|
||||||
private static string GetMiscDisplay(LibraryBook libraryBook)
|
|
||||||
{
|
|
||||||
var details = new List<string>();
|
|
||||||
|
|
||||||
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
|
#endregion
|
||||||
|
|
||||||
~LibraryBookEntry()
|
~LibraryBookEntry()
|
||||||
|
|||||||
@ -39,10 +39,10 @@
|
|||||||
this.productsGrid.Name = "productsGrid";
|
this.productsGrid.Name = "productsGrid";
|
||||||
this.productsGrid.Size = new System.Drawing.Size(1510, 380);
|
this.productsGrid.Size = new System.Drawing.Size(1510, 380);
|
||||||
this.productsGrid.TabIndex = 0;
|
this.productsGrid.TabIndex = 0;
|
||||||
this.productsGrid.LiberateClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
|
this.productsGrid.LiberateClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
|
||||||
this.productsGrid.CoverClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_CoverClicked);
|
this.productsGrid.CoverClicked += new LibationWinForms.GridView.GridEntryClickedEventHandler(this.productsGrid_CoverClicked);
|
||||||
this.productsGrid.DetailsClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
|
this.productsGrid.DetailsClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
|
||||||
this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
|
this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.GridEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
|
||||||
this.productsGrid.VisibleCountChanged += new System.EventHandler<int>(this.productsGrid_VisibleCountChanged);
|
this.productsGrid.VisibleCountChanged += new System.EventHandler<int>(this.productsGrid_VisibleCountChanged);
|
||||||
//
|
//
|
||||||
// ProductsDisplay
|
// ProductsDisplay
|
||||||
|
|||||||
@ -29,7 +29,7 @@ namespace LibationWinForms.GridView
|
|||||||
#region Button controls
|
#region Button controls
|
||||||
|
|
||||||
private ImageDisplay imageDisplay;
|
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 picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
|
||||||
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
|
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
|
||||||
@ -52,7 +52,7 @@ namespace LibationWinForms.GridView
|
|||||||
imageDisplay.CoverPicture = await picDlTask;
|
imageDisplay.CoverPicture = await picDlTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void productsGrid_DescriptionClicked(LibraryBookEntry liveGridEntry, Rectangle cellRectangle)
|
private void productsGrid_DescriptionClicked(GridEntry liveGridEntry, Rectangle cellRectangle)
|
||||||
{
|
{
|
||||||
var displayWindow = new DescriptionDisplay
|
var displayWindow = new DescriptionDisplay
|
||||||
{
|
{
|
||||||
@ -103,7 +103,6 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay));
|
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -10,23 +10,24 @@ using System.Windows.Forms;
|
|||||||
|
|
||||||
namespace LibationWinForms.GridView
|
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 partial class ProductsGrid : UserControl
|
||||||
{
|
{
|
||||||
public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry);
|
|
||||||
public delegate void LibraryBookEntryRectangleClickedEventHandler(LibraryBookEntry liveGridEntry, Rectangle cellRectangle);
|
|
||||||
|
|
||||||
/// <summary>Number of visible rows has changed</summary>
|
/// <summary>Number of visible rows has changed</summary>
|
||||||
public event EventHandler<int> VisibleCountChanged;
|
public event EventHandler<int> VisibleCountChanged;
|
||||||
public event LibraryBookEntryClickedEventHandler LiberateClicked;
|
public event LibraryBookEntryClickedEventHandler LiberateClicked;
|
||||||
public event LibraryBookEntryClickedEventHandler CoverClicked;
|
public event GridEntryClickedEventHandler CoverClicked;
|
||||||
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
||||||
public event LibraryBookEntryRectangleClickedEventHandler DescriptionClicked;
|
public event GridEntryRectangleClickedEventHandler DescriptionClicked;
|
||||||
public new event EventHandler<ScrollEventArgs> Scroll;
|
public new event EventHandler<ScrollEventArgs> Scroll;
|
||||||
|
|
||||||
private GridEntryBindingList bindingList;
|
private GridEntryBindingList bindingList;
|
||||||
internal IEnumerable<LibraryBookEntry> GetVisible()
|
internal IEnumerable<LibraryBookEntry> GetVisible()
|
||||||
=> bindingList
|
=> bindingList
|
||||||
.LibraryBooks();
|
.BookEntries();
|
||||||
|
|
||||||
public ProductsGrid()
|
public ProductsGrid()
|
||||||
{
|
{
|
||||||
@ -61,16 +62,23 @@ namespace LibationWinForms.GridView
|
|||||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||||
CoverClicked?.Invoke(lbEntry);
|
CoverClicked?.Invoke(lbEntry);
|
||||||
}
|
}
|
||||||
else if (entry is SeriesEntry sEntry && e.ColumnIndex == liberateGVColumn.Index)
|
else if (entry is SeriesEntry sEntry)
|
||||||
{
|
{
|
||||||
if (sEntry.Liberate.Expanded)
|
if (e.ColumnIndex == liberateGVColumn.Index)
|
||||||
bindingList.CollapseItem(sEntry);
|
{
|
||||||
else
|
if (sEntry.Liberate.Expanded)
|
||||||
bindingList.ExpandItem(sEntry);
|
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<LibraryBook> dbBooks)
|
internal void BindToGrid(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
var geList = dbBooks.Where(b => b.Book.ContentType is not ContentType.Episode).Select(b => new LibraryBookEntry(b)).Cast<GridEntry>().ToList();
|
var geList = dbBooks.Where(lb => lb.IsProduct()).Select(b => new LibraryBookEntry(b)).Cast<GridEntry>().ToList();
|
||||||
|
|
||||||
var episodes = dbBooks.Where(b => b.IsEpisodeChild()).ToList();
|
var parents = dbBooks.Where(lb => lb.IsEpisodeParent());
|
||||||
|
var episodes = dbBooks.Where(lb => lb.IsEpisodeChild());
|
||||||
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)
|
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.Add(seriesEntry);
|
||||||
geList.AddRange(seriesEntry.Children);
|
geList.AddRange(seriesEntry.Children);
|
||||||
@ -98,79 +110,47 @@ namespace LibationWinForms.GridView
|
|||||||
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
|
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
|
||||||
bindingList.CollapseAll();
|
bindingList.CollapseAll();
|
||||||
syncBindingSource.DataSource = bindingList;
|
syncBindingSource.DataSource = bindingList;
|
||||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void UpdateGrid(List<LibraryBook> dbBooks)
|
internal void UpdateGrid(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
|
#region Add new or update existing grid entries
|
||||||
|
|
||||||
|
//Remove filter prior to adding/updating boooks
|
||||||
string existingFilter = syncBindingSource.Filter;
|
string existingFilter = syncBindingSource.Filter;
|
||||||
Filter(null);
|
Filter(null);
|
||||||
|
|
||||||
bindingList.SuspendFilteringOnUpdate = true;
|
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();
|
var allEntries = bindingList.AllItems().BookEntries();
|
||||||
foreach (var libraryBook in dbBooks)
|
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 (libraryBook.IsEpisodeChild())
|
||||||
if (existingItem is null)
|
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
|
||||||
{
|
else if (libraryBook.IsProduct())
|
||||||
if (libraryBook.IsEpisodeChild())
|
AddOrUpdateBook(libraryBook, existingEntry);
|
||||||
{
|
}
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bindingList.SuspendFilteringOnUpdate = false;
|
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);
|
Filter(existingFilter);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
// remove deleted from grid.
|
// remove deleted from grid.
|
||||||
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
||||||
var removedBooks =
|
var removedBooks =
|
||||||
bindingList
|
bindingList
|
||||||
.AllItems()
|
.AllItems()
|
||||||
.LibraryBooks()
|
.BookEntries()
|
||||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
|
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
|
||||||
|
|
||||||
//Remove books in series from their parents' Children list
|
//Remove books in series from their parents' Children list
|
||||||
@ -190,7 +170,69 @@ namespace LibationWinForms.GridView
|
|||||||
//no need to re-filter for removed books
|
//no need to re-filter for removed books
|
||||||
bindingList.Remove(removed);
|
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<SeriesEntry> seriesEntries, IEnumerable<LibraryBook> 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
|
#endregion
|
||||||
@ -207,7 +249,7 @@ namespace LibationWinForms.GridView
|
|||||||
syncBindingSource.Filter = searchString;
|
syncBindingSource.Filter = searchString;
|
||||||
|
|
||||||
if (visibleCount != bindingList.Count)
|
if (visibleCount != bindingList.Count)
|
||||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
58
Source/LibationWinForms/GridView/QueryExtensions.cs
Normal file
58
Source/LibationWinForms/GridView/QueryExtensions.cs
Normal file
@ -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<LibraryBookEntry> BookEntries(this IEnumerable<GridEntry> gridEntries)
|
||||||
|
=> gridEntries.OfType<LibraryBookEntry>();
|
||||||
|
|
||||||
|
public static IEnumerable<SeriesEntry> SeriesEntries(this IEnumerable<GridEntry> gridEntries)
|
||||||
|
=> gridEntries.OfType<SeriesEntry>();
|
||||||
|
|
||||||
|
public static T? FindByAsin<T>(this IEnumerable<T> gridEntries, string audibleProductID) where T : GridEntry
|
||||||
|
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
|
||||||
|
|
||||||
|
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> 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<GridEntry> 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<LibraryBook> 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
|
||||||
|
}
|
||||||
@ -6,101 +6,79 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace LibationWinForms.GridView
|
namespace LibationWinForms.GridView
|
||||||
{
|
{
|
||||||
|
/// <summary>The View Model for a LibraryBook that is ContentType.Parent</summary>
|
||||||
public class SeriesEntry : GridEntry
|
public class SeriesEntry : GridEntry
|
||||||
{
|
{
|
||||||
public List<LibraryBookEntry> Children { get; init; }
|
public List<LibraryBookEntry> Children { get; } = new();
|
||||||
public override DateTime DateAdded => Children.Max(c => c.DateAdded);
|
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 string DisplayTags { get; } = string.Empty;
|
||||||
|
|
||||||
public override LiberateButtonStatus Liberate { get; }
|
public override LiberateButtonStatus Liberate { get; }
|
||||||
|
|
||||||
protected override Book Book => SeriesBook.Book;
|
private SeriesEntry(LibraryBook parent)
|
||||||
|
|
||||||
private SeriesBook SeriesBook { get; set; }
|
|
||||||
|
|
||||||
private SeriesEntry(SeriesBook seriesBook)
|
|
||||||
{
|
{
|
||||||
|
LibraryBook = parent;
|
||||||
Liberate = new LiberateButtonStatus { IsSeries = true };
|
Liberate = new LiberateButtonStatus { IsSeries = true };
|
||||||
SeriesIndex = seriesBook.Index;
|
SeriesIndex = -1;
|
||||||
}
|
}
|
||||||
public SeriesEntry(SeriesBook seriesBook, IEnumerable<LibraryBook> children) : this(seriesBook)
|
|
||||||
|
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children) : this(parent)
|
||||||
{
|
{
|
||||||
Children = children.Select(c => new LibraryBookEntry(c) { Parent = this }).OrderBy(c => c.SeriesIndex).ToList();
|
Children = children
|
||||||
SetSeriesBook(seriesBook);
|
.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 } };
|
Children = new() { new LibraryBookEntry(child) { Parent = this } };
|
||||||
SetSeriesBook(seriesBook);
|
|
||||||
|
UpdateSeries(parent);
|
||||||
|
LoadCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetSeriesBook(SeriesBook seriesBook)
|
public void UpdateSeries(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
SeriesBook = seriesBook;
|
LibraryBook = libraryBook;
|
||||||
LoadCover();
|
|
||||||
|
|
||||||
// Immutable properties
|
// Immutable properties
|
||||||
{
|
{
|
||||||
Title = SeriesBook.Series.Name;
|
Title = Book.Title;
|
||||||
Series = SeriesBook.Series.Name;
|
Series = Book.SeriesNames();
|
||||||
|
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||||
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
|
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
|
||||||
|
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||||
Authors = Book.AuthorNames();
|
Authors = Book.AuthorNames();
|
||||||
Narrators = Book.NarratorNames();
|
Narrators = Book.NarratorNames();
|
||||||
Category = string.Join(" > ", Book.CategoriesNames());
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Create getters for all member object values by name</summary>
|
/// <summary>Create getters for all member object values by name</summary>
|
||||||
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
|
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
|
||||||
{
|
{
|
||||||
{ nameof(Title), () => Book.SeriesSortable() },
|
{ nameof(Title), () => Book.TitleSortable() },
|
||||||
{ nameof(Series), () => Book.SeriesSortable() },
|
{ nameof(Series), () => Book.SeriesSortable() },
|
||||||
{ nameof(Length), () => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes) },
|
{ 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(PurchaseDate), () => Children.Min(c => c.LibraryBook.DateAdded) },
|
||||||
{ nameof(ProductRating), () => Children.Average(c => c.LibraryBook.Book.Rating.FirstScore()) },
|
{ nameof(ProductRating), () => Book.Rating.FirstScore() },
|
||||||
{ nameof(Authors), () => string.Empty },
|
{ nameof(Authors), () => Authors },
|
||||||
{ nameof(Narrators), () => string.Empty },
|
{ nameof(Narrators), () => Narrators },
|
||||||
{ nameof(Description), () => string.Empty },
|
{ nameof(Description), () => Description },
|
||||||
{ nameof(Category), () => string.Empty },
|
{ nameof(Category), () => Category },
|
||||||
{ nameof(Misc), () => string.Empty },
|
{ nameof(Misc), () => Misc },
|
||||||
{ nameof(DisplayTags), () => string.Empty },
|
{ nameof(DisplayTags), () => string.Empty },
|
||||||
{ nameof(Liberate), () => Liberate },
|
{ nameof(Liberate), () => Liberate },
|
||||||
{ nameof(DateAdded), () => DateAdded },
|
{ nameof(DateAdded), () => DateAdded },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user