From e8a320dac97e545767f5386c4b474bb47ddda497 Mon Sep 17 00:00:00 2001
From: Michael Bucari-Tovo
Date: Sun, 22 May 2022 20:00:41 -0600
Subject: [PATCH] Add grid categories
---
Source/AppScaffolding/AppScaffolding.csproj | 2 +-
Source/DataLayer/EfClasses/Rating.cs | 2 +-
.../Dialogs/RemoveBooksDialog.cs | 11 +-
Source/LibationWinForms/Form1.cs | 4 +-
.../LibationWinForms/LibationWinForms.csproj | 6 +
.../Properties/Resources.Designer.cs | 20 ++
.../Properties/Resources.resx | 6 +
Source/LibationWinForms/Resources/minus.png | Bin 0 -> 425 bytes
Source/LibationWinForms/Resources/plus.png | Bin 0 -> 689 bytes
.../grid/FilterableSortableBindingList.cs | 75 ++++-
Source/LibationWinForms/grid/GridEntry.cs | 271 +++---------------
.../LiberateDataGridViewImageButtonColumn.cs | 22 +-
.../LibationWinForms/grid/LibraryBookEntry.cs | 253 ++++++++++++++++
.../grid/MasterDataGridView.cs | 26 ++
.../grid/ProductsDisplay.Designer.cs | 46 +++
.../LibationWinForms/grid/ProductsDisplay.cs | 152 ++++++++++
.../grid/ProductsDisplay.resx | 63 ++++
.../grid/ProductsGrid.Designer.cs | 28 +-
Source/LibationWinForms/grid/ProductsGrid.cs | 193 +++++++------
.../LibationWinForms/grid/ProductsGrid.resx | 6 -
Source/LibationWinForms/grid/SeriesEntry.cs | 90 ++++++
.../grid/SortableBindingList1.cs | 111 +++++++
22 files changed, 1008 insertions(+), 379 deletions(-)
create mode 100644 Source/LibationWinForms/Resources/minus.png
create mode 100644 Source/LibationWinForms/Resources/plus.png
create mode 100644 Source/LibationWinForms/grid/LibraryBookEntry.cs
create mode 100644 Source/LibationWinForms/grid/MasterDataGridView.cs
create mode 100644 Source/LibationWinForms/grid/ProductsDisplay.Designer.cs
create mode 100644 Source/LibationWinForms/grid/ProductsDisplay.cs
create mode 100644 Source/LibationWinForms/grid/ProductsDisplay.resx
create mode 100644 Source/LibationWinForms/grid/SeriesEntry.cs
create mode 100644 Source/LibationWinForms/grid/SortableBindingList1.cs
diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj
index 37d7e6ca..db37ad59 100644
--- a/Source/AppScaffolding/AppScaffolding.csproj
+++ b/Source/AppScaffolding/AppScaffolding.csproj
@@ -3,7 +3,7 @@
net6.0-windows
- 7.7.1.1
+ 7.7.0.14
diff --git a/Source/DataLayer/EfClasses/Rating.cs b/Source/DataLayer/EfClasses/Rating.cs
index 761633be..18b0cd05 100644
--- a/Source/DataLayer/EfClasses/Rating.cs
+++ b/Source/DataLayer/EfClasses/Rating.cs
@@ -12,7 +12,7 @@ namespace DataLayer
public float StoryRating { get; private set; }
private Rating() { }
- internal Rating(float overallRating, float performanceRating, float storyRating)
+ public Rating(float overallRating, float performanceRating, float storyRating)
{
OverallRating = overallRating;
PerformanceRating = performanceRating;
diff --git a/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs b/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs
index ecd56188..bb1759dc 100644
--- a/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs
+++ b/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs
@@ -121,10 +121,8 @@ namespace LibationWinForms.Dialogs
}
}
- internal class RemovableGridEntry : GridEntry
+ internal class RemovableGridEntry : LibraryBookEntry
{
- private static readonly IComparer BoolComparer = new ObjectComparer();
-
private bool _remove = false;
public RemovableGridEntry(LibraryBook libraryBook) : base(libraryBook) { }
@@ -147,12 +145,5 @@ namespace LibationWinForms.Dialogs
return Remove;
return base.GetMemberValue(memberName);
}
-
- public override IComparer GetMemberComparer(Type memberType)
- {
- if (memberType == typeof(bool))
- return BoolComparer;
- return base.GetMemberComparer(memberType);
- }
}
}
diff --git a/Source/LibationWinForms/Form1.cs b/Source/LibationWinForms/Form1.cs
index 04150167..0c2f3ce1 100644
--- a/Source/LibationWinForms/Form1.cs
+++ b/Source/LibationWinForms/Form1.cs
@@ -12,7 +12,7 @@ namespace LibationWinForms
{
public partial class Form1 : Form
{
- private ProductsGrid productsGrid { get; }
+ private ProductsDisplay productsGrid { get; }
public Form1()
{
@@ -26,7 +26,7 @@ namespace LibationWinForms
// Failed to create component 'ProductsGrid'. The error message follows:
// 'Microsoft.DotNet.DesignTools.Client.DesignToolsServerException: Object reference not set to an instance of an object.
// Since the designer's choking on it, I'm keeping it below the DesignMode check to be safe
- productsGrid = new ProductsGrid { Dock = DockStyle.Fill };
+ productsGrid = new ProductsDisplay { Dock = DockStyle.Fill };
gridPanel.Controls.Add(productsGrid);
}
diff --git a/Source/LibationWinForms/LibationWinForms.csproj b/Source/LibationWinForms/LibationWinForms.csproj
index 2ecd019b..979db0ff 100644
--- a/Source/LibationWinForms/LibationWinForms.csproj
+++ b/Source/LibationWinForms/LibationWinForms.csproj
@@ -45,6 +45,9 @@
+
+ UserControl
+
True
True
@@ -53,6 +56,9 @@
+
+ Designer
+
ResXFileCodeGenerator
Resources.Designer.cs
diff --git a/Source/LibationWinForms/Properties/Resources.Designer.cs b/Source/LibationWinForms/Properties/Resources.Designer.cs
index 468d9e4d..f3e881e2 100644
--- a/Source/LibationWinForms/Properties/Resources.Designer.cs
+++ b/Source/LibationWinForms/Properties/Resources.Designer.cs
@@ -229,5 +229,25 @@ namespace LibationWinForms.Properties {
return ((System.Drawing.Bitmap)(obj));
}
}
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap minus {
+ get {
+ object obj = ResourceManager.GetObject("minus", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap plus {
+ get {
+ object obj = ResourceManager.GetObject("plus", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
}
}
diff --git a/Source/LibationWinForms/Properties/Resources.resx b/Source/LibationWinForms/Properties/Resources.resx
index 0a1ae67a..95b3b8a0 100644
--- a/Source/LibationWinForms/Properties/Resources.resx
+++ b/Source/LibationWinForms/Properties/Resources.resx
@@ -169,4 +169,10 @@
..\Resources\liberate_yellow_pdf_yes.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+ ..\Resources\minus.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\plus.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
\ No newline at end of file
diff --git a/Source/LibationWinForms/Resources/minus.png b/Source/LibationWinForms/Resources/minus.png
new file mode 100644
index 0000000000000000000000000000000000000000..bfc2cf4abc65ecbd5ac663ccf26584d356818814
GIT binary patch
literal 425
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9GG!XV7ZFl&wkP*5S+
zBgmJ5p-Pp3p`n?9;pcxK{gQ#9)PRBERRRNp)eHs(@%%~gN8K1081+3}978H@y}4=V
zci2I|;o|w$z5E2+II)LiOI_s3muj?ww!P_=H)F*;mrLYpJ|UCeg?qu<@w|S{tuv+bcHU-fW7@+c{XpA$&9{y-rIUB^
zzhmMpsCStDK&*m!Uc>8kWjB^{Z#m%XSC^5yK{keQ@qd9&LLzv*An>66$ajXYck|g#
T{9jWB3}yyTS3j3^P6Z9gktZjYPrOP?j1FFJAGKiK|}SEq(q{#f~j35QNyI>IO9)6v_Ms3UX2lW&J@
zzw5U&A*PtNH}|%0-fV3D+@(b;y0>dKqX7fk0S1W%t>|RgW~=A#8pJNJv^j*kth;VB
zeM0Mm;&);@82TDxV;l4n81^vqD-{VBv&>;$eqi6aeatL3n9IHt`nkyl<`fe%>pk
ze|FW~XTSAIJXI(s^M-%eKXLiJ`&QQlGG#D%9y_~SQRS7mDnt0D`Bxc1Rx&UrFz_@0
z?f);=P<(E+P{{umN(v_p<9z=Vf$2@a>Anae6tJ%OKsja!;tdv%j$+VniLwli{?fd#|k4rt&
zT>tbYQ>0PY#jqPs&oFWxePWnjxptcJo6RXY_osip!W?t3aL?1NsgXYoM87$`pb8Yut(
literal 0
HcmV?d00001
diff --git a/Source/LibationWinForms/grid/FilterableSortableBindingList.cs b/Source/LibationWinForms/grid/FilterableSortableBindingList.cs
index 1e896e0b..e3bcaad0 100644
--- a/Source/LibationWinForms/grid/FilterableSortableBindingList.cs
+++ b/Source/LibationWinForms/grid/FilterableSortableBindingList.cs
@@ -19,14 +19,16 @@ namespace LibationWinForms
* Remove is overridden to ensure that removed items are removed from
* the base list (visible items) as well as the FilterRemoved list.
*/
- internal class FilterableSortableBindingList : SortableBindingList, IBindingListView
+ internal class FilterableSortableBindingList : SortableBindingList1, IBindingListView
{
///
/// Items that were removed from the base list due to filtering
///
private readonly List FilterRemoved = new();
private string FilterString;
+ private LibationSearchEngine.SearchResultSet SearchResults;
public FilterableSortableBindingList(IEnumerable enumeration) : base(enumeration) { }
+ public FilterableSortableBindingList() : base(new List()) { }
public bool SupportsFiltering => true;
public string Filter { get => FilterString; set => ApplyFilter(value); }
@@ -48,7 +50,14 @@ namespace LibationWinForms
}
/// All items in the list, including those filtered out.
- public List AllItems() => Items.Concat(FilterRemoved).ToList();
+ public List AllItems()
+ {
+ var allItems = Items.Concat(FilterRemoved);
+
+ var series = allItems.Where(i => i is SeriesEntry).Cast().SelectMany(s => s.Children);
+
+ return series.Concat(allItems).ToList();
+ }
private void ApplyFilter(string filterString)
{
@@ -57,18 +66,49 @@ namespace LibationWinForms
FilterString = filterString;
- var searchResults = SearchEngineCommands.Search(filterString);
- var filteredOut = Items.ExceptBy(searchResults.Docs.Select(d => d.ProductId), ge => ge.AudibleProductId);
+ SearchResults = SearchEngineCommands.Search(filterString);
+ var filteredOut = Items.Where(i => i is LibraryBookEntry).Cast().ExceptBy(SearchResults.Docs.Select(d => d.ProductId), ge => ge.AudibleProductId).Cast().ToList();
- for (int i = Items.Count - 1; i >= 0; i--)
+ var parents = Items.Where(i => i is SeriesEntry).Cast();
+
+ foreach (var p in parents)
{
- if (filteredOut.Contains(Items[i]))
+ if (p.Children.Cast().ExceptBy(SearchResults.Docs.Select(d => d.ProductId), ge => ge.AudibleProductId).Count() == p.Children.Count)
{
- FilterRemoved.Add(Items[i]);
- Items.RemoveAt(i);
- base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, i));
+ //Don't show series whose episodes have all been filtered out
+ filteredOut.Add(p);
}
}
+
+ for (int i = 0; i < filteredOut.Count; i++)
+ {
+ FilterRemoved.Add(filteredOut[i]);
+ base.Remove(filteredOut[i]);
+ }
+ }
+
+ public void CollapseItem(SeriesEntry sEntry)
+ {
+ foreach (var item in Items.Where(b => b is LibraryBookEntry).Cast().Where(b => b.Parent == sEntry).ToList())
+ base.Remove(item);
+
+ sEntry.Liberate.Expanded = false;
+ }
+
+ public void ExpandItem(SeriesEntry sEntry)
+ {
+ var sindex = Items.IndexOf(sEntry);
+ var children = sEntry.Children.Cast().ToList();
+ for (int i = 0; i < children.Count; i++)
+ {
+ if (SearchResults is null || SearchResults.Docs.Any(d=> d.ProductId == children[i].AudibleProductId))
+ Insert(++sindex, children[i]);
+ else
+ {
+ FilterRemoved.Add(children[i]);
+ }
+ }
+ sEntry.Liberate.Expanded = true;
}
public void RemoveFilter()
@@ -77,18 +117,27 @@ namespace LibationWinForms
int visibleCount = Items.Count;
for (int i = 0; i < FilterRemoved.Count; i++)
- base.InsertItem(i + visibleCount, FilterRemoved[i]);
- OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
+ {
+ if (FilterRemoved[i].Parent is null || FilterRemoved[i].Parent.Liberate.Expanded)
+ base.InsertItem(i + visibleCount, FilterRemoved[i]);
+ }
FilterRemoved.Clear();
if (IsSortedCore)
Sort();
else
- //No user-defined sort is applied, so do default sorting by date added, descending
- ((List)Items).Sort((i1, i2) => i2.LibraryBook.DateAdded.CompareTo(i1.LibraryBook.DateAdded));
+ //No user sort is applied, so do default sorting by PurchaseDate, descending
+ {
+ Comparer.PropertyName = nameof(GridEntry.DateAdded);
+ Comparer.Direction = ListSortDirection.Descending;
+ Sort();
+ }
+
+ OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
FilterString = null;
+ SearchResults = null;
}
}
}
diff --git a/Source/LibationWinForms/grid/GridEntry.cs b/Source/LibationWinForms/grid/GridEntry.cs
index 3e02dc93..a9d86b41 100644
--- a/Source/LibationWinForms/grid/GridEntry.cs
+++ b/Source/LibationWinForms/grid/GridEntry.cs
@@ -1,101 +1,65 @@
-using System;
+using DataLayer;
+using Dinah.Core.DataBinding;
+using Dinah.Core.Drawing;
+using LibationFileManager;
+using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
-using System.Linq;
-using ApplicationServices;
-using DataLayer;
-using Dinah.Core.DataBinding;
-using Dinah.Core;
-using Dinah.Core.Drawing;
-using LibationFileManager;
-using System.Threading.Tasks;
namespace LibationWinForms
{
- ///
- /// The View Model for a LibraryBook
- ///
- internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
+ public interface IHierarchical where T : class
{
- #region implementation properties NOT exposed to the view
- // hide from public fields from Data Source GUI with [Browsable(false)]
+ T Parent { get; }
+ List Children { get; }
+ }
+ internal class LiberateStatus
+ {
+ public LiberatedStatus BookStatus;
+ public LiberatedStatus? PdfStatus;
+ public bool IsSeries;
+ public bool Expanded;
+ }
- [Browsable(false)]
- public string AudibleProductId => Book.AudibleProductId;
- [Browsable(false)]
- public LibraryBook LibraryBook { get; private set; }
- [Browsable(false)]
- public string LongDescription { get; private set; }
- #endregion
+ internal abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable, IHierarchical
+ {
+ protected abstract Book Book { get; }
- #region Model properties exposed to the view
private Image _cover;
-
- private DateTime lastStatusUpdate = default;
- private LiberatedStatus _bookStatus;
- private LiberatedStatus? _pdfStatus;
+ #region Model properties exposed to the view
public Image Cover
{
get => _cover;
- private set
+ protected set
{
_cover = value;
NotifyPropertyChanged();
}
}
-
- public string ProductRating { get; private set; }
- public string PurchaseDate { get; private set; }
- public string MyRating { get; private set; }
- public string Series { get; private set; }
- public string Title { get; private set; }
- public string Length { get; private set; }
- public string Authors { get; private set; }
- public string Narrators { get; private set; }
- public string Category { get; private set; }
- public string Misc { get; private set; }
- public string Description { get; private set; }
- public 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 (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
- {
- get
- {
- //Cache these statuses for faster sorting.
- if ((DateTime.Now - lastStatusUpdate).TotalSeconds > 2)
- {
- _bookStatus = LibraryCommands.Liberated_Status(LibraryBook.Book);
- _pdfStatus = LibraryCommands.Pdf_Status(LibraryBook.Book);
- lastStatusUpdate = DateTime.Now;
- }
- return (_bookStatus, _pdfStatus);
- }
- }
+ public GridEntry Parent { get; set; }
+ public List Children { get; set; }
+ public abstract string ProductRating { get; protected set; }
+ public abstract string PurchaseDate { get; protected set; }
+ public abstract DateTime DateAdded { get; }
+ 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 LiberateStatus Liberate { get; }
+ public abstract object GetMemberValue(string memberName);
#endregion
+ public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
- // alias
- private Book Book => LibraryBook.Book;
-
- public GridEntry(LibraryBook libraryBook) => setLibraryBook(libraryBook);
-
- public void UpdateLibraryBook(LibraryBook libraryBook)
+ protected void LoadCover()
{
- if (AudibleProductId != libraryBook.Book.AudibleProductId)
- throw new Exception("Invalid grid entry update. IDs must match");
-
- setLibraryBook(libraryBook);
-
- NotifyPropertyChanged();
- }
-
- private void setLibraryBook(LibraryBook libraryBook)
- {
- LibraryBook = libraryBook;
- _memberValues = CreateMemberValueDictionary();
-
// Get cover art. If it's default, subscribe to PictureCached
{
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
@@ -106,24 +70,6 @@ namespace LibationWinForms
// Mutable property. Set the field so PropertyChanged isn't fired.
_cover = ImageReader.ToImage(picture);
}
-
- // Immutable properties
- {
- Title = Book.Title;
- Series = Book.SeriesNames();
- Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
- MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
- PurchaseDate = 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);
- }
-
- UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
@@ -135,154 +81,19 @@ namespace LibationWinForms
}
}
- #region detect changes to the model, update the view, and save to database.
-
- ///
- /// This event handler receives notifications from the model that it has changed.
- /// Save to the database and notify the view that it's changed.
- ///
- private void UserDefinedItem_ItemChanged(object sender, string itemName)
- {
- var udi = sender as UserDefinedItem;
-
- if (udi.Book.AudibleProductId != Book.AudibleProductId)
- return;
-
- switch (itemName)
- {
- case nameof(udi.Tags):
- Book.UserDefinedItem.Tags = udi.Tags;
- NotifyPropertyChanged(nameof(DisplayTags));
- break;
- case nameof(udi.BookStatus):
- Book.UserDefinedItem.BookStatus = udi.BookStatus;
- _bookStatus = udi.BookStatus;
- NotifyPropertyChanged(nameof(Liberate));
- break;
- case nameof(udi.PdfStatus):
- Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
- _pdfStatus = udi.PdfStatus;
- NotifyPropertyChanged(nameof(Liberate));
- break;
- }
- }
-
- /// Save edits to the database
- public void Commit(string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
- {
- // validate
- if (DisplayTags.EqualsInsensitive(newTags) &&
- Liberate.BookStatus == bookStatus &&
- Liberate.PdfStatus == pdfStatus)
- return;
-
- // update cache
- _bookStatus = bookStatus;
- _pdfStatus = pdfStatus;
-
- // set + save
- Book.UserDefinedItem.Tags = newTags;
- Book.UserDefinedItem.BookStatus = bookStatus;
- Book.UserDefinedItem.PdfStatus = pdfStatus;
- LibraryCommands.UpdateUserDefinedItem(Book);
- }
-
- #endregion
-
- #region Data Sorting
- // These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
- // Used by Dinah.Core.DataBinding.SortableBindingList for all sorting
- public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
- public virtual IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
-
- private Dictionary> _memberValues { get; set; }
-
- ///
- /// Create getters for all member object values by name
- ///
- private Dictionary> CreateMemberValueDictionary() => new()
- {
- { nameof(Title), () => Book.TitleSortable() },
- { nameof(Series), () => Book.SeriesSortable() },
- { nameof(Length), () => Book.LengthInMinutes },
- { nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore() },
- { nameof(PurchaseDate), () => LibraryBook.DateAdded },
- { nameof(ProductRating), () => Book.Rating.FirstScore() },
- { nameof(Authors), () => Authors },
- { nameof(Narrators), () => Narrators },
- { nameof(Description), () => Description },
- { nameof(Category), () => Category },
- { nameof(Misc), () => Misc },
- { nameof(DisplayTags), () => DisplayTags },
- { nameof(Liberate), () => Liberate.BookStatus }
- };
-
// 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(LiberatedStatus), new ObjectComparer() },
};
- #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
-
~GridEntry()
{
- UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
diff --git a/Source/LibationWinForms/grid/LiberateDataGridViewImageButtonColumn.cs b/Source/LibationWinForms/grid/LiberateDataGridViewImageButtonColumn.cs
index e5ab82b8..159b1e4a 100644
--- a/Source/LibationWinForms/grid/LiberateDataGridViewImageButtonColumn.cs
+++ b/Source/LibationWinForms/grid/LiberateDataGridViewImageButtonColumn.cs
@@ -20,15 +20,27 @@ namespace LibationWinForms
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
- if (value is (LiberatedStatus, LiberatedStatus) or (LiberatedStatus, null))
+ if (value is LiberateStatus status)
{
- var (bookState, pdfState) = ((LiberatedStatus bookState, LiberatedStatus? pdfState))value;
+ if (status.IsSeries)
+ {
+ var imageName = status.Expanded ? "minus" : "plus";
+ var text = status.Expanded ? "Click to Collpase" : "Click to Expand";
- (string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(bookState, pdfState);
+ var bmp = (Bitmap)Properties.Resources.ResourceManager.GetObject(imageName);
+ DrawButtonImage(graphics, bmp, cellBounds);
- DrawButtonImage(graphics, buttonImage, cellBounds);
+ ToolTipText = text;
- ToolTipText = mouseoverText;
+ }
+ else
+ {
+ (string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(status.BookStatus, status.PdfStatus);
+
+ DrawButtonImage(graphics, buttonImage, cellBounds);
+
+ ToolTipText = mouseoverText;
+ }
}
}
diff --git a/Source/LibationWinForms/grid/LibraryBookEntry.cs b/Source/LibationWinForms/grid/LibraryBookEntry.cs
new file mode 100644
index 00000000..e87ae630
--- /dev/null
+++ b/Source/LibationWinForms/grid/LibraryBookEntry.cs
@@ -0,0 +1,253 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Linq;
+using ApplicationServices;
+using DataLayer;
+using Dinah.Core.DataBinding;
+using Dinah.Core;
+using Dinah.Core.Drawing;
+using LibationFileManager;
+using System.Threading.Tasks;
+
+namespace LibationWinForms
+{
+ ///
+ /// The View Model for a LibraryBook
+ ///
+ internal 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
+
+ // alias
+ protected override Book Book => LibraryBook.Book;
+ #region Model properties exposed to the view
+
+ private DateTime lastStatusUpdate = default;
+ private LiberatedStatus _bookStatus;
+ private LiberatedStatus? _pdfStatus;
+
+ public override DateTime DateAdded => LibraryBook.DateAdded;
+ 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 LiberateStatus Liberate
+ {
+ get
+ {
+ //Cache these statuses for faster sorting.
+ if ((DateTime.Now - lastStatusUpdate).TotalSeconds > 2)
+ {
+ _bookStatus = LibraryCommands.Liberated_Status(LibraryBook.Book);
+ _pdfStatus = LibraryCommands.Pdf_Status(LibraryBook.Book);
+ lastStatusUpdate = DateTime.Now;
+ }
+ return new LiberateStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
+ }
+ }
+ #endregion
+
+
+ public LibraryBookEntry(LibraryBook libraryBook) => setLibraryBook(libraryBook);
+
+ public void UpdateLibraryBook(LibraryBook libraryBook)
+ {
+ if (AudibleProductId != libraryBook.Book.AudibleProductId)
+ throw new Exception("Invalid grid entry update. IDs must match");
+
+ setLibraryBook(libraryBook);
+
+ NotifyPropertyChanged();
+ }
+
+ private void setLibraryBook(LibraryBook libraryBook)
+ {
+ LibraryBook = libraryBook;
+ _memberValues = CreateMemberValueDictionary();
+
+ LoadCover();
+
+ // Immutable properties
+ {
+ Title = Book.Title;
+ Series = Book.SeriesNames();
+ Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
+ MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
+ PurchaseDate = 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);
+ }
+
+ UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
+ }
+
+
+ #region detect changes to the model, update the view, and save to database.
+
+ ///
+ /// This event handler receives notifications from the model that it has changed.
+ /// Save to the database and notify the view that it's changed.
+ ///
+ private void UserDefinedItem_ItemChanged(object sender, string itemName)
+ {
+ var udi = sender as UserDefinedItem;
+
+ if (udi.Book.AudibleProductId != Book.AudibleProductId)
+ return;
+
+ switch (itemName)
+ {
+ case nameof(udi.Tags):
+ Book.UserDefinedItem.Tags = udi.Tags;
+ NotifyPropertyChanged(nameof(DisplayTags));
+ break;
+ case nameof(udi.BookStatus):
+ Book.UserDefinedItem.BookStatus = udi.BookStatus;
+ _bookStatus = udi.BookStatus;
+ NotifyPropertyChanged(nameof(Liberate));
+ break;
+ case nameof(udi.PdfStatus):
+ Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
+ _pdfStatus = udi.PdfStatus;
+ NotifyPropertyChanged(nameof(Liberate));
+ break;
+ }
+ }
+
+ /// Save edits to the database
+ public void Commit(string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
+ {
+ // validate
+ if (DisplayTags.EqualsInsensitive(newTags) &&
+ Liberate.BookStatus == bookStatus &&
+ Liberate.PdfStatus == pdfStatus)
+ return;
+
+ // update cache
+ _bookStatus = bookStatus;
+ _pdfStatus = pdfStatus;
+
+ // set + save
+ Book.UserDefinedItem.Tags = newTags;
+ Book.UserDefinedItem.BookStatus = bookStatus;
+ Book.UserDefinedItem.PdfStatus = pdfStatus;
+ LibraryCommands.UpdateUserDefinedItem(Book);
+ }
+
+ #endregion
+
+ #region Data Sorting
+ // These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
+ // Used by Dinah.Core.DataBinding.SortableBindingList for all sorting
+ public override object GetMemberValue(string memberName) => _memberValues[memberName]();
+
+ private Dictionary> _memberValues { get; set; }
+
+ ///
+ /// Create getters for all member object values by name
+ ///
+ private Dictionary> CreateMemberValueDictionary() => new()
+ {
+ { nameof(Title), () => Book.TitleSortable() },
+ { nameof(Series), () => Book.SeriesSortable() },
+ { nameof(Length), () => Book.LengthInMinutes },
+ { nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore() },
+ { nameof(PurchaseDate), () => LibraryBook.DateAdded },
+ { nameof(ProductRating), () => Book.Rating.FirstScore() },
+ { nameof(Authors), () => Authors },
+ { nameof(Narrators), () => Narrators },
+ { nameof(Description), () => Description },
+ { nameof(Category), () => Category },
+ { nameof(Misc), () => Misc },
+ { nameof(DisplayTags), () => DisplayTags },
+ { nameof(Liberate), () => Liberate.BookStatus },
+ { 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()
+ {
+ UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
+ }
+ }
+}
diff --git a/Source/LibationWinForms/grid/MasterDataGridView.cs b/Source/LibationWinForms/grid/MasterDataGridView.cs
new file mode 100644
index 00000000..fd3b481a
--- /dev/null
+++ b/Source/LibationWinForms/grid/MasterDataGridView.cs
@@ -0,0 +1,26 @@
+using Dinah.Core.Windows.Forms;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace LibationWinForms
+{
+
+ internal class MasterDataGridView : DataGridView
+ {
+ internal delegate void LibraryBookEntryClickedEventHandler(DataGridViewCellEventArgs e, LibraryBookEntry entry);
+ public event LibraryBookEntryClickedEventHandler LibraryBookEntryClicked;
+ public MasterDataGridView()
+ {
+
+ }
+
+
+ public GridEntry getGridEntry(int rowIndex) => this.GetBoundItem(rowIndex);
+
+ }
+}
diff --git a/Source/LibationWinForms/grid/ProductsDisplay.Designer.cs b/Source/LibationWinForms/grid/ProductsDisplay.Designer.cs
new file mode 100644
index 00000000..d15ad85c
--- /dev/null
+++ b/Source/LibationWinForms/grid/ProductsDisplay.Designer.cs
@@ -0,0 +1,46 @@
+namespace LibationWinForms
+{
+ partial class ProductsDisplay
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.SuspendLayout();
+ //
+ // ProductsDisplay
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ this.Name = "ProductsDisplay";
+ this.Size = new System.Drawing.Size(1510, 380);
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/LibationWinForms/grid/ProductsDisplay.cs b/Source/LibationWinForms/grid/ProductsDisplay.cs
new file mode 100644
index 00000000..e0e0fd58
--- /dev/null
+++ b/Source/LibationWinForms/grid/ProductsDisplay.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using ApplicationServices;
+using DataLayer;
+using Dinah.Core.Windows.Forms;
+using FileLiberator;
+using LibationFileManager;
+using LibationWinForms.Dialogs;
+
+namespace LibationWinForms
+{
+
+ #region // legacy instructions to update data_grid_view
+ // INSTRUCTIONS TO UPDATE DATA_GRID_VIEW
+ // - delete current DataGridView
+ // - view > other windows > data sources
+ // - refresh
+ // OR
+ // - Add New Data Source
+ // Object. Next
+ // LibationWinForms
+ // AudibleDTO
+ // GridEntry
+ // - go to Design view
+ // - click on Data Sources > ProductItem. dropdown: DataGridView
+ // - drag/drop ProductItem on design surface
+ //
+ // as of august 2021 this does not work in vs2019 with .net5 projects
+ // VS has improved since then with .net6+ but I haven't checked again
+ #endregion
+
+
+ public partial class ProductsDisplay : UserControl
+ {
+ public event EventHandler LiberateClicked;
+ /// Number of visible rows has changed
+ public event EventHandler VisibleCountChanged;
+
+ // alias
+
+ private ProductsGrid grid;
+
+ public ProductsDisplay()
+ {
+ InitializeComponent();
+
+ grid = new ProductsGrid();
+ grid.Dock = DockStyle.Fill;
+ Controls.Add(grid);
+
+ if (this.DesignMode)
+ return;
+
+ grid.LiberateClicked += (_, book) => LiberateClicked?.Invoke(this, book.LibraryBook);
+ grid.DetailsClicked += Grid_DetailsClicked;
+ grid.CoverClicked += Grid_CoverClicked;
+ grid.DescriptionClicked += Grid_DescriptionClicked1;
+ }
+
+ #region Button controls
+
+ private ImageDisplay imageDisplay;
+ private async void Grid_CoverClicked(DataGridViewCellEventArgs e, LibraryBookEntry liveGridEntry)
+ {
+ var picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
+ var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
+
+ (_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
+ var windowTitle = $"{liveGridEntry.Title} - Cover";
+
+ if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
+ {
+ imageDisplay = new ImageDisplay();
+ imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
+ imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
+ imageDisplay.Show(this);
+ }
+
+ imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(liveGridEntry.LibraryBook);
+ imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(liveGridEntry.LibraryBook, ".jpg"));
+ imageDisplay.Text = windowTitle;
+ imageDisplay.CoverPicture = initialImageBts;
+ imageDisplay.CoverPicture = await picDlTask;
+ }
+
+ private void Grid_DescriptionClicked1(DataGridViewCellEventArgs e, LibraryBookEntry liveGridEntry, Rectangle cellRectangle)
+ {
+ var displayWindow = new DescriptionDisplay
+ {
+ SpawnLocation = PointToScreen(cellRectangle.Location + new Size(cellRectangle.Width, 0)),
+ DescriptionText = liveGridEntry.LongDescription,
+ BorderThickness = 2,
+ };
+
+ void CloseWindow(object o, EventArgs e)
+ {
+ displayWindow.Close();
+ }
+
+ grid.Scroll += CloseWindow;
+ displayWindow.FormClosed += (_, _) => grid.Scroll -= CloseWindow;
+ displayWindow.Show(this);
+ }
+
+
+ private void Grid_DetailsClicked(DataGridViewCellEventArgs e, LibraryBookEntry liveGridEntry)
+ {
+ var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
+ if (bookDetailsForm.ShowDialog() == DialogResult.OK)
+ liveGridEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
+ }
+
+ #endregion
+
+ #region UI display functions
+
+ private bool hasBeenDisplayed;
+ public event EventHandler InitialLoaded;
+ public void Display()
+ {
+ // don't return early if lib size == 0. this will not update correctly if all books are removed
+ var lib = DbContexts.GetLibrary_Flat_NoTracking();
+
+ if (!hasBeenDisplayed)
+ {
+ // bind
+ grid.bindToGrid(lib);
+ hasBeenDisplayed = true;
+ InitialLoaded?.Invoke(this, new());
+ VisibleCountChanged?.Invoke(this, grid.GetVisible().Count());
+ }
+ else
+ grid.updateGrid(lib);
+
+ }
+
+ #endregion
+
+ #region Filter
+
+ public void Filter(string searchString)
+ => grid.Filter(searchString);
+
+ #endregion
+
+ internal List GetVisible() => grid.GetVisible().ToList();
+ }
+}
diff --git a/Source/LibationWinForms/grid/ProductsDisplay.resx b/Source/LibationWinForms/grid/ProductsDisplay.resx
new file mode 100644
index 00000000..be5db7af
--- /dev/null
+++ b/Source/LibationWinForms/grid/ProductsDisplay.resx
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 81
+
+
\ No newline at end of file
diff --git a/Source/LibationWinForms/grid/ProductsGrid.Designer.cs b/Source/LibationWinForms/grid/ProductsGrid.Designer.cs
index b3eaecd5..27496d79 100644
--- a/Source/LibationWinForms/grid/ProductsGrid.Designer.cs
+++ b/Source/LibationWinForms/grid/ProductsGrid.Designer.cs
@@ -64,20 +64,20 @@
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
- this.liberateGVColumn,
- this.coverGVColumn,
- this.titleGVColumn,
- this.authorsGVColumn,
- this.narratorsGVColumn,
- this.lengthGVColumn,
- this.seriesGVColumn,
- this.descriptionGVColumn,
- this.categoryGVColumn,
- this.productRatingGVColumn,
- this.purchaseDateGVColumn,
- this.myRatingGVColumn,
- this.miscGVColumn,
- this.tagAndDetailsGVColumn});
+ this.liberateGVColumn,
+ this.coverGVColumn,
+ this.titleGVColumn,
+ this.authorsGVColumn,
+ this.narratorsGVColumn,
+ this.lengthGVColumn,
+ this.seriesGVColumn,
+ this.descriptionGVColumn,
+ this.categoryGVColumn,
+ this.productRatingGVColumn,
+ this.purchaseDateGVColumn,
+ this.myRatingGVColumn,
+ this.miscGVColumn,
+ this.tagAndDetailsGVColumn});
this.gridEntryDataGridView.ContextMenuStrip = this.contextMenuStrip1;
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
diff --git a/Source/LibationWinForms/grid/ProductsGrid.cs b/Source/LibationWinForms/grid/ProductsGrid.cs
index 690cb00d..2d585f27 100644
--- a/Source/LibationWinForms/grid/ProductsGrid.cs
+++ b/Source/LibationWinForms/grid/ProductsGrid.cs
@@ -2,14 +2,11 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
-using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using DataLayer;
using Dinah.Core.Windows.Forms;
-using FileLiberator;
using LibationFileManager;
-using LibationWinForms.Dialogs;
namespace LibationWinForms
{
@@ -36,7 +33,17 @@ namespace LibationWinForms
public partial class ProductsGrid : UserControl
{
- public event EventHandler LiberateClicked;
+
+ internal delegate void LibraryBookEntryClickedEventHandler(DataGridViewCellEventArgs e, LibraryBookEntry liveGridEntry);
+ internal delegate void LibraryBookEntryRectangleClickedEventHandler(DataGridViewCellEventArgs e, LibraryBookEntry liveGridEntry, Rectangle cellRectangle);
+ internal event LibraryBookEntryClickedEventHandler LiberateClicked;
+ internal event LibraryBookEntryClickedEventHandler CoverClicked;
+ internal event LibraryBookEntryClickedEventHandler DetailsClicked;
+ internal event LibraryBookEntryRectangleClickedEventHandler DescriptionClicked;
+ public new event EventHandler Scroll;
+
+ private FilterableSortableBindingList bindingList;
+
/// Number of visible rows has changed
public event EventHandler VisibleCountChanged;
@@ -53,8 +60,14 @@ namespace LibationWinForms
EnableDoubleBuffering();
_dataGridView.CellContentClick += DataGridView_CellContentClick;
+ _dataGridView.Scroll += (_, s) => Scroll?.Invoke(this, s);
- this.Load += ProductsGrid_Load;
+ Load += ProductsGrid_Load;
+ }
+
+ private void ProductsGrid_Scroll(object sender, ScrollEventArgs e)
+ {
+ throw new NotImplementedException();
}
private void EnableDoubleBuffering()
@@ -66,117 +79,70 @@ namespace LibationWinForms
#region Button controls
- private async void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
+ private void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
// handle grid button click: https://stackoverflow.com/a/13687844
if (e.RowIndex < 0)
return;
- if (e.ColumnIndex == liberateGVColumn.Index)
- Liberate_Click(getGridEntry(e.RowIndex));
- else if (e.ColumnIndex == tagAndDetailsGVColumn.Index)
- Details_Click(getGridEntry(e.RowIndex));
- else if (e.ColumnIndex == descriptionGVColumn.Index)
- Description_Click(getGridEntry(e.RowIndex), _dataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
- else if (e.ColumnIndex == coverGVColumn.Index)
- await Cover_Click(getGridEntry(e.RowIndex));
- }
-
- private ImageDisplay imageDisplay;
- private async Task Cover_Click(GridEntry liveGridEntry)
- {
- var picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
- var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
-
- (_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
- var windowTitle = $"{liveGridEntry.Title} - Cover";
-
- if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
+ var entry = getGridEntry(e.RowIndex);
+ if (entry is LibraryBookEntry lbEntry)
{
- imageDisplay = new ImageDisplay();
- imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
- imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
- imageDisplay.Show(this);
+ if (e.ColumnIndex == liberateGVColumn.Index)
+ LiberateClicked?.Invoke(e, lbEntry);
+ else if (e.ColumnIndex == tagAndDetailsGVColumn.Index && entry is LibraryBookEntry)
+ DetailsClicked?.Invoke(e, lbEntry);
+ else if (e.ColumnIndex == descriptionGVColumn.Index)
+ DescriptionClicked?.Invoke(e, lbEntry, _dataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
+ else if (e.ColumnIndex == coverGVColumn.Index)
+ CoverClicked?.Invoke(e, lbEntry);
}
-
- imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(liveGridEntry.LibraryBook);
- imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(liveGridEntry.LibraryBook, ".jpg"));
- imageDisplay.Text = windowTitle;
- imageDisplay.CoverPicture = initialImageBts;
- imageDisplay.CoverPicture = await picDlTask;
- }
-
- private void Description_Click(GridEntry liveGridEntry, Rectangle cellDisplay)
- {
- var displayWindow = new DescriptionDisplay
+ else if (entry is SeriesEntry sEntry && e.ColumnIndex == liberateGVColumn.Index)
{
- SpawnLocation = PointToScreen(cellDisplay.Location + new Size(cellDisplay.Width, 0)),
- DescriptionText = liveGridEntry.LongDescription,
- BorderThickness = 2,
- };
+ if (sEntry.Liberate.Expanded)
+ bindingList.CollapseItem(sEntry);
+ else
+ bindingList.ExpandItem(sEntry);
- void CloseWindow(object o, EventArgs e)
- {
- displayWindow.Close();
+ sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate));
}
-
- _dataGridView.Scroll += CloseWindow;
- displayWindow.FormClosed += (_, _) => _dataGridView.Scroll -= CloseWindow;
- displayWindow.Show(this);
}
- private void Liberate_Click(GridEntry liveGridEntry)
- {
- LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
- }
-
- private static void Details_Click(GridEntry liveGridEntry)
- {
- var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
- if (bookDetailsForm.ShowDialog() == DialogResult.OK)
- liveGridEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
- }
+ private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem(rowIndex);
#endregion
#region UI display functions
- private FilterableSortableBindingList bindingList;
-
- private bool hasBeenDisplayed;
- public event EventHandler InitialLoaded;
- public void Display()
+ internal void bindToGrid(List dbBooks)
{
- // don't return early if lib size == 0. this will not update correctly if all books are removed
- var lib = DbContexts.GetLibrary_Flat_NoTracking();
+ var geList = dbBooks.Where(b => b.Book.ContentType is not ContentType.Episode).Select(b => new LibraryBookEntry(b)).Cast().ToList();
- if (!hasBeenDisplayed)
+ var episodes = dbBooks.Where(b => b.Book.ContentType is ContentType.Episode).ToList();
+
+ var series = episodes.Select(lb => lb.Book.SeriesLink.First()).DistinctBy(s => s.Series).ToList();
+
+ foreach (var s in series)
{
- // bind
- bindToGrid(lib);
- hasBeenDisplayed = true;
- InitialLoaded?.Invoke(this, new());
- VisibleCountChanged?.Invoke(this, bindingList.Count);
+ var seriesEntry = new SeriesEntry();
+ seriesEntry.Children = episodes.Where(lb => lb.Book.SeriesLink.First().Series == s.Book.SeriesLink.First().Series).Select(lb => new LibraryBookEntry(lb) { Parent = seriesEntry }).Cast().ToList();
+
+ seriesEntry.setSeriesBook(s);
+ geList.Add(seriesEntry);
}
- else
- updateGrid(lib);
- }
-
- private void bindToGrid(List dbBooks)
- {
- bindingList = new FilterableSortableBindingList(dbBooks.OrderByDescending(lb => lb.DateAdded).Select(lb => new GridEntry(lb)));
+ bindingList = new FilterableSortableBindingList(geList.OrderByDescending(ge => ge.DateAdded));
gridEntryBindingSource.DataSource = bindingList;
}
- private void updateGrid(List dbBooks)
+ internal void updateGrid(List dbBooks)
{
int visibleCount = bindingList.Count;
string existingFilter = gridEntryBindingSource.Filter;
//Add absent books to grid, or update current books
- var allItmes = bindingList.AllItems();
+ var allItmes = bindingList.AllItems().Where(i => i is LibraryBookEntry).Cast();
for (var i = dbBooks.Count - 1; i >= 0; i--)
{
var libraryBook = dbBooks[i];
@@ -184,10 +150,37 @@ namespace LibationWinForms
// add new to top
if (existingItem is null)
- bindingList.Insert(0, new GridEntry(libraryBook));
+ {
+ var lb = new LibraryBookEntry(libraryBook);
+
+ if (libraryBook.Book.ContentType is ContentType.Episode)
+ {
+ //Find the series that libraryBook, if it exists
+ var series = bindingList.AllItems().Where(i => i is SeriesEntry).Cast().FirstOrDefault(i => libraryBook.Book.SeriesLink.Any(s => s.Series.Name == i.Series));
+
+ if (series is null)
+ {
+ //Series doesn't exist yet, so create and add it
+ var newSeries = new SeriesEntry { Children = new List { lb } };
+ newSeries.setSeriesBook(libraryBook.Book.SeriesLink.First());
+ lb.Parent = newSeries;
+ newSeries.Liberate.Expanded = true;
+ bindingList.Insert(0, newSeries);
+ }
+ else
+ {
+ lb.Parent = series;
+ series.Children.Add(lb);
+ }
+ }
+ //Add the new product
+ bindingList.Insert(0, lb);
+ }
// update existing
else
+ {
existingItem.UpdateLibraryBook(libraryBook);
+ }
}
if (bindingList.Count != visibleCount)
@@ -199,13 +192,22 @@ namespace LibationWinForms
// 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 =
+ var removedBooks =
bindingList
.AllItems()
- .ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId)
- .ToList();
+ .Where(i => i is LibraryBookEntry)
+ .Cast()
+ .ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
- foreach (var removed in removedBooks)
+ //Remove series that have no children
+ var removedSeries =
+ bindingList
+ .AllItems()
+ .Where(i => i is SeriesEntry)
+ .Cast()
+ .Where(i => removedBooks.Count(r => r.Series == i.Series) == i.Children.Count);
+
+ foreach (var removed in removedBooks.Cast().Concat(removedSeries))
//no need to re-filter for removed books
bindingList.Remove(removed);
@@ -232,12 +234,11 @@ namespace LibationWinForms
#endregion
- internal List GetVisible()
+ internal IEnumerable GetVisible()
=> bindingList
- .Select(row => row.LibraryBook)
- .ToList();
-
- private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem(rowIndex);
+ .Where(row => row is LibraryBookEntry)
+ .Cast()
+ .Select(row => row.LibraryBook);
#region Column Customizations
@@ -293,8 +294,6 @@ namespace LibationWinForms
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index);
}
-
- base.OnVisibleChanged(e);
}
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
diff --git a/Source/LibationWinForms/grid/ProductsGrid.resx b/Source/LibationWinForms/grid/ProductsGrid.resx
index 8a560af5..be5db7af 100644
--- a/Source/LibationWinForms/grid/ProductsGrid.resx
+++ b/Source/LibationWinForms/grid/ProductsGrid.resx
@@ -57,12 +57,6 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
- 17, 17
-
-
- 197, 17
-
81
diff --git a/Source/LibationWinForms/grid/SeriesEntry.cs b/Source/LibationWinForms/grid/SeriesEntry.cs
new file mode 100644
index 00000000..6a1e51b8
--- /dev/null
+++ b/Source/LibationWinForms/grid/SeriesEntry.cs
@@ -0,0 +1,90 @@
+using DataLayer;
+using Dinah.Core;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace LibationWinForms
+{
+ internal class SeriesEntry : GridEntry
+ {
+ public override DateTime DateAdded => Children.Max(c => c.DateAdded);
+ 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.Empty;
+
+ public override LiberateStatus Liberate => _liberate;
+
+ protected override Book Book => SeriesBook.Book;
+
+ private SeriesBook SeriesBook { get; set; }
+
+ private LiberateStatus _liberate = new LiberateStatus { IsSeries = true };
+ public void setSeriesBook(SeriesBook seriesBook)
+ {
+ SeriesBook = seriesBook;
+ _memberValues = CreateMemberValueDictionary();
+ LoadCover();
+
+ // Immutable properties
+ {
+ var childLB = Children.Cast();
+ int bookLenMins = childLB.Sum(c => c.LibraryBook.Book.LengthInMinutes);
+
+ var myAverageRating = new Rating(childLB.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.OverallRating), childLB.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.PerformanceRating), childLB.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.StoryRating));
+ var productAverageRating = new Rating(childLB.Average(c => c.LibraryBook.Book.Rating.OverallRating), childLB.Average(c => c.LibraryBook.Book.Rating.PerformanceRating), childLB.Average(c => c.LibraryBook.Book.Rating.StoryRating));
+
+
+ Title = SeriesBook.Series.Name;
+ Series = SeriesBook.Series.Name;
+ Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
+ MyRating = myAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
+ PurchaseDate = childLB.Min(c => c.LibraryBook.DateAdded).ToString("d");
+ ProductRating = productAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
+ Authors = Book.AuthorNames();
+ Narrators = Book.NarratorNames();
+ Category = string.Join(" > ", Book.CategoriesNames());
+ }
+ }
+
+ // These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
+ // Used by Dinah.Core.DataBinding.SortableBindingList for all sorting
+ public override object GetMemberValue(string memberName) => _memberValues[memberName]();
+
+ private Dictionary> _memberValues { get; set; }
+
+ ///
+ /// Create getters for all member object values by name
+ ///
+ private Dictionary> CreateMemberValueDictionary() => new()
+ {
+ { nameof(Title), () => Book.SeriesSortable() },
+ { nameof(Series), () => Book.SeriesSortable() },
+ { nameof(Length), () => Children.Cast().Sum(c=>c.LibraryBook.Book.LengthInMinutes) },
+ { nameof(MyRating), () => Children.Cast().Average(c=>c.LibraryBook.Book.UserDefinedItem.Rating.FirstScore()) },
+ { nameof(PurchaseDate), () => Children.Cast().Min(c=>c.LibraryBook.DateAdded) },
+ { nameof(ProductRating), () => Children.Cast().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(DisplayTags), () => string.Empty },
+ { nameof(Liberate), () => Liberate.BookStatus },
+ { nameof(DateAdded), () => DateAdded },
+ };
+ }
+}
diff --git a/Source/LibationWinForms/grid/SortableBindingList1.cs b/Source/LibationWinForms/grid/SortableBindingList1.cs
new file mode 100644
index 00000000..7d12819f
--- /dev/null
+++ b/Source/LibationWinForms/grid/SortableBindingList1.cs
@@ -0,0 +1,111 @@
+using Dinah.Core.DataBinding;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace LibationWinForms
+{
+ internal class SortableBindingList1 : BindingList where T : class, IMemberComparable, IHierarchical
+ {
+ private bool isSorted;
+ private ListSortDirection listSortDirection;
+ private PropertyDescriptor propertyDescriptor;
+
+ public SortableBindingList1() : base(new List()) { }
+ public SortableBindingList1(IEnumerable enumeration) : base(new List(enumeration)) { }
+
+ protected MemberComparer Comparer { get; } = new();
+ protected override bool SupportsSortingCore => true;
+ protected override bool SupportsSearchingCore => true;
+ protected override bool IsSortedCore => isSorted;
+ protected override PropertyDescriptor SortPropertyCore => propertyDescriptor;
+ protected override ListSortDirection SortDirectionCore => listSortDirection;
+
+ protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
+ {
+ Comparer.PropertyName = property.Name;
+ Comparer.Direction = direction;
+
+ Sort();
+
+ propertyDescriptor = property;
+ listSortDirection = direction;
+ isSorted = true;
+
+ OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
+ }
+
+ protected void Sort()
+ {
+ List itemsList = (List)Items;
+
+ //Array.Sort() and List.Sort() are unstable sorts. OrderBy is stable.
+ var sortedItems = itemsList.OrderBy((ge) => ge, Comparer).ToList();
+
+ var children = sortedItems.Where(i => i.Parent is not null).ToList();
+ var parents = sortedItems.Where(i => i.Children is not null).ToList();
+
+ //Top Level items
+ var topLevelItems = sortedItems.Except(children);
+
+ itemsList.Clear();
+ itemsList.AddRange(topLevelItems);
+
+ foreach (var p in parents)
+ {
+ var pIndex = itemsList.IndexOf(p);
+ foreach (var c in children.Where(c=> c.Parent == p))
+ itemsList.Insert(++pIndex, c);
+ }
+ }
+
+ protected override void OnListChanged(ListChangedEventArgs e)
+ {
+ if (isSorted &&
+ ((e.ListChangedType == ListChangedType.ItemChanged && e.PropertyDescriptor == SortPropertyCore) ||
+ e.ListChangedType == ListChangedType.ItemAdded))
+ {
+ var item = Items[e.NewIndex];
+ Sort();
+ var newIndex = Items.IndexOf(item);
+
+ base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemMoved, newIndex, e.NewIndex));
+ }
+ else
+ base.OnListChanged(e);
+ }
+
+ protected override void RemoveSortCore()
+ {
+ isSorted = false;
+ propertyDescriptor = base.SortPropertyCore;
+ listSortDirection = base.SortDirectionCore;
+
+ OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
+ }
+
+ protected override int FindCore(PropertyDescriptor property, object key)
+ {
+ int count = Count;
+
+ System.Collections.IComparer valueComparer = null;
+
+ for (int i = 0; i < count; ++i)
+ {
+ var element = this[i];
+ var elemValue = element.GetMemberValue(property.Name);
+ valueComparer ??= element.GetMemberComparer(elemValue.GetType());
+
+ if (valueComparer.Compare(elemValue, key) == 0)
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+ }
+}