Add grid categories

This commit is contained in:
Michael Bucari-Tovo 2022-05-22 20:00:41 -06:00
parent 3cb43e5d3e
commit e8a320dac9
22 changed files with 1008 additions and 379 deletions

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<Version>7.7.1.1</Version>
<Version>7.7.0.14</Version>
</PropertyGroup>
<ItemGroup>

View File

@ -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;

View File

@ -121,10 +121,8 @@ namespace LibationWinForms.Dialogs
}
}
internal class RemovableGridEntry : GridEntry
internal class RemovableGridEntry : LibraryBookEntry
{
private static readonly IComparer BoolComparer = new ObjectComparer<bool>();
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);
}
}
}

View File

@ -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);
}

View File

@ -45,6 +45,9 @@
</ItemGroup>
<ItemGroup>
<Compile Update="grid\ProductsGrid.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@ -53,6 +56,9 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="grid\ProductsGrid.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>

View File

@ -229,5 +229,25 @@ namespace LibationWinForms.Properties {
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap minus {
get {
object obj = ResourceManager.GetObject("minus", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap plus {
get {
object obj = ResourceManager.GetObject("plus", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
}
}

View File

@ -169,4 +169,10 @@
<data name="liberate_yellow_pdf_yes" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\liberate_yellow_pdf_yes.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="minus" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\minus.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="plus" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\plus.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

View File

@ -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<GridEntry>, IBindingListView
internal class FilterableSortableBindingList : SortableBindingList1<GridEntry>, IBindingListView
{
/// <summary>
/// Items that were removed from the base list due to filtering
/// </summary>
private readonly List<GridEntry> FilterRemoved = new();
private string FilterString;
private LibationSearchEngine.SearchResultSet SearchResults;
public FilterableSortableBindingList(IEnumerable<GridEntry> enumeration) : base(enumeration) { }
public FilterableSortableBindingList() : base(new List<GridEntry>()) { }
public bool SupportsFiltering => true;
public string Filter { get => FilterString; set => ApplyFilter(value); }
@ -48,7 +50,14 @@ namespace LibationWinForms
}
/// <returns>All items in the list, including those filtered out.</returns>
public List<GridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
public List<GridEntry> AllItems()
{
var allItems = Items.Concat(FilterRemoved);
var series = allItems.Where(i => i is SeriesEntry).Cast<SeriesEntry>().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<LibraryBookEntry>().ExceptBy(SearchResults.Docs.Select(d => d.ProductId), ge => ge.AudibleProductId).Cast<GridEntry>().ToList();
for (int i = Items.Count - 1; i >= 0; i--)
var parents = Items.Where(i => i is SeriesEntry).Cast<SeriesEntry>();
foreach (var p in parents)
{
if (filteredOut.Contains(Items[i]))
if (p.Children.Cast<LibraryBookEntry>().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<LibraryBookEntry>().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<LibraryBookEntry>().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<GridEntry>)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;
}
}
}

View File

@ -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
{
/// <summary>
/// The View Model for a LibraryBook
/// </summary>
internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
public interface IHierarchical<T> 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<T> 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<GridEntry>
{
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<GridEntry> 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.
/// <summary>
/// This event handler receives notifications from the model that it has changed.
/// Save to the database and notify the view that it's changed.
/// </summary>
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;
}
}
/// <summary>Save edits to the database</summary>
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<T> for all sorting
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
public virtual IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
private Dictionary<string, Func<object>> _memberValues { get; set; }
/// <summary>
/// Create getters for all member object values by name
/// </summary>
private Dictionary<string, Func<object>> 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<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(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
};
#endregion
#region Static library display functions
/// <summary>
/// This information should not change during <see cref="GridEntry"/> 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="GridEntry"/> 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
~GridEntry()
{
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}

View File

@ -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;
}
}
}

View File

@ -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
{
/// <summary>
/// The View Model for a LibraryBook
/// </summary>
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.
/// <summary>
/// This event handler receives notifications from the model that it has changed.
/// Save to the database and notify the view that it's changed.
/// </summary>
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;
}
}
/// <summary>Save edits to the database</summary>
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<T> for all sorting
public override object GetMemberValue(string memberName) => _memberValues[memberName]();
private Dictionary<string, Func<object>> _memberValues { get; set; }
/// <summary>
/// Create getters for all member object values by name
/// </summary>
private Dictionary<string, Func<object>> 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
/// <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
~LibraryBookEntry()
{
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
}
}
}

View File

@ -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<GridEntry>(rowIndex);
}
}

View File

@ -0,0 +1,46 @@
namespace LibationWinForms
{
partial class ProductsDisplay
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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
}
}

View File

@ -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<LibraryBook> LiberateClicked;
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> 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<LibraryBook> GetVisible() => grid.GetVisible().ToList();
}
}

View File

@ -0,0 +1,63 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>81</value>
</metadata>
</root>

View File

@ -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;

View File

@ -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<LibraryBook> 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<ScrollEventArgs> Scroll;
private FilterableSortableBindingList bindingList;
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> 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<GridEntry>(rowIndex);
#endregion
#region UI display functions
private FilterableSortableBindingList bindingList;
private bool hasBeenDisplayed;
public event EventHandler InitialLoaded;
public void Display()
internal void bindToGrid(List<LibraryBook> 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<GridEntry>().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<GridEntry>().ToList();
seriesEntry.setSeriesBook(s);
geList.Add(seriesEntry);
}
else
updateGrid(lib);
}
private void bindToGrid(List<LibraryBook> 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<LibraryBook> dbBooks)
internal void updateGrid(List<LibraryBook> 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<LibraryBookEntry>();
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<SeriesEntry>().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<GridEntry> { 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<LibraryBookEntry>()
.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<SeriesEntry>()
.Where(i => removedBooks.Count(r => r.Series == i.Series) == i.Children.Count);
foreach (var removed in removedBooks.Cast<GridEntry>().Concat(removedSeries))
//no need to re-filter for removed books
bindingList.Remove(removed);
@ -232,12 +234,11 @@ namespace LibationWinForms
#endregion
internal List<LibraryBook> GetVisible()
internal IEnumerable<LibraryBook> GetVisible()
=> bindingList
.Select(row => row.LibraryBook)
.ToList();
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
.Where(row => row is LibraryBookEntry)
.Cast<LibraryBookEntry>()
.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)

View File

@ -57,12 +57,6 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="gridEntryBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="contextMenuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>197, 17</value>
</metadata>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>81</value>
</metadata>

View File

@ -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<LibraryBookEntry>();
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<T> for all sorting
public override object GetMemberValue(string memberName) => _memberValues[memberName]();
private Dictionary<string, Func<object>> _memberValues { get; set; }
/// <summary>
/// Create getters for all member object values by name
/// </summary>
private Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
{
{ nameof(Title), () => Book.SeriesSortable() },
{ nameof(Series), () => Book.SeriesSortable() },
{ nameof(Length), () => Children.Cast<LibraryBookEntry>().Sum(c=>c.LibraryBook.Book.LengthInMinutes) },
{ nameof(MyRating), () => Children.Cast<LibraryBookEntry>().Average(c=>c.LibraryBook.Book.UserDefinedItem.Rating.FirstScore()) },
{ nameof(PurchaseDate), () => Children.Cast<LibraryBookEntry>().Min(c=>c.LibraryBook.DateAdded) },
{ nameof(ProductRating), () => Children.Cast<LibraryBookEntry>().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 },
};
}
}

View File

@ -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<T> : BindingList<T> where T : class, IMemberComparable, IHierarchical<T>
{
private bool isSorted;
private ListSortDirection listSortDirection;
private PropertyDescriptor propertyDescriptor;
public SortableBindingList1() : base(new List<T>()) { }
public SortableBindingList1(IEnumerable<T> enumeration) : base(new List<T>(enumeration)) { }
protected MemberComparer<T> 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<T> itemsList = (List<T>)Items;
//Array.Sort() and List<T>.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;
}
}
}