Add grid categories
This commit is contained in:
parent
3cb43e5d3e
commit
e8a320dac9
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0-windows</TargetFramework>
|
<TargetFramework>net6.0-windows</TargetFramework>
|
||||||
<Version>7.7.1.1</Version>
|
<Version>7.7.0.14</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ namespace DataLayer
|
|||||||
public float StoryRating { get; private set; }
|
public float StoryRating { get; private set; }
|
||||||
|
|
||||||
private Rating() { }
|
private Rating() { }
|
||||||
internal Rating(float overallRating, float performanceRating, float storyRating)
|
public Rating(float overallRating, float performanceRating, float storyRating)
|
||||||
{
|
{
|
||||||
OverallRating = overallRating;
|
OverallRating = overallRating;
|
||||||
PerformanceRating = performanceRating;
|
PerformanceRating = performanceRating;
|
||||||
|
|||||||
@ -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;
|
private bool _remove = false;
|
||||||
public RemovableGridEntry(LibraryBook libraryBook) : base(libraryBook) { }
|
public RemovableGridEntry(LibraryBook libraryBook) : base(libraryBook) { }
|
||||||
|
|
||||||
@ -147,12 +145,5 @@ namespace LibationWinForms.Dialogs
|
|||||||
return Remove;
|
return Remove;
|
||||||
return base.GetMemberValue(memberName);
|
return base.GetMemberValue(memberName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IComparer GetMemberComparer(Type memberType)
|
|
||||||
{
|
|
||||||
if (memberType == typeof(bool))
|
|
||||||
return BoolComparer;
|
|
||||||
return base.GetMemberComparer(memberType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
public partial class Form1 : Form
|
public partial class Form1 : Form
|
||||||
{
|
{
|
||||||
private ProductsGrid productsGrid { get; }
|
private ProductsDisplay productsGrid { get; }
|
||||||
|
|
||||||
public Form1()
|
public Form1()
|
||||||
{
|
{
|
||||||
@ -26,7 +26,7 @@ namespace LibationWinForms
|
|||||||
// Failed to create component 'ProductsGrid'. The error message follows:
|
// Failed to create component 'ProductsGrid'. The error message follows:
|
||||||
// 'Microsoft.DotNet.DesignTools.Client.DesignToolsServerException: Object reference not set to an instance of an object.
|
// '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
|
// 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);
|
gridPanel.Controls.Add(productsGrid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Update="grid\ProductsGrid.cs">
|
||||||
|
<SubType>UserControl</SubType>
|
||||||
|
</Compile>
|
||||||
<Compile Update="Properties\Resources.Designer.cs">
|
<Compile Update="Properties\Resources.Designer.cs">
|
||||||
<DesignTime>True</DesignTime>
|
<DesignTime>True</DesignTime>
|
||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
@ -53,6 +56,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Update="grid\ProductsGrid.resx">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Update="Properties\Resources.resx">
|
<EmbeddedResource Update="Properties\Resources.resx">
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
|
|||||||
@ -229,5 +229,25 @@ namespace LibationWinForms.Properties {
|
|||||||
return ((System.Drawing.Bitmap)(obj));
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,4 +169,10 @@
|
|||||||
<data name="liberate_yellow_pdf_yes" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
<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>
|
<value>..\Resources\liberate_yellow_pdf_yes.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
BIN
Source/LibationWinForms/Resources/minus.png
Normal file
BIN
Source/LibationWinForms/Resources/minus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 425 B |
BIN
Source/LibationWinForms/Resources/plus.png
Normal file
BIN
Source/LibationWinForms/Resources/plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 689 B |
@ -19,14 +19,16 @@ namespace LibationWinForms
|
|||||||
* Remove is overridden to ensure that removed items are removed from
|
* Remove is overridden to ensure that removed items are removed from
|
||||||
* the base list (visible items) as well as the FilterRemoved list.
|
* the base list (visible items) as well as the FilterRemoved list.
|
||||||
*/
|
*/
|
||||||
internal class FilterableSortableBindingList : SortableBindingList<GridEntry>, IBindingListView
|
internal class FilterableSortableBindingList : SortableBindingList1<GridEntry>, IBindingListView
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Items that were removed from the base list due to filtering
|
/// Items that were removed from the base list due to filtering
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly List<GridEntry> FilterRemoved = new();
|
private readonly List<GridEntry> FilterRemoved = new();
|
||||||
private string FilterString;
|
private string FilterString;
|
||||||
|
private LibationSearchEngine.SearchResultSet SearchResults;
|
||||||
public FilterableSortableBindingList(IEnumerable<GridEntry> enumeration) : base(enumeration) { }
|
public FilterableSortableBindingList(IEnumerable<GridEntry> enumeration) : base(enumeration) { }
|
||||||
|
public FilterableSortableBindingList() : base(new List<GridEntry>()) { }
|
||||||
|
|
||||||
public bool SupportsFiltering => true;
|
public bool SupportsFiltering => true;
|
||||||
public string Filter { get => FilterString; set => ApplyFilter(value); }
|
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>
|
/// <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)
|
private void ApplyFilter(string filterString)
|
||||||
{
|
{
|
||||||
@ -57,18 +66,49 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
FilterString = filterString;
|
FilterString = filterString;
|
||||||
|
|
||||||
var searchResults = SearchEngineCommands.Search(filterString);
|
SearchResults = SearchEngineCommands.Search(filterString);
|
||||||
var filteredOut = Items.ExceptBy(searchResults.Docs.Select(d => d.ProductId), ge => ge.AudibleProductId);
|
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]);
|
//Don't show series whose episodes have all been filtered out
|
||||||
Items.RemoveAt(i);
|
filteredOut.Add(p);
|
||||||
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, i));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
public void RemoveFilter()
|
||||||
@ -77,18 +117,27 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
int visibleCount = Items.Count;
|
int visibleCount = Items.Count;
|
||||||
for (int i = 0; i < FilterRemoved.Count; i++)
|
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();
|
FilterRemoved.Clear();
|
||||||
|
|
||||||
if (IsSortedCore)
|
if (IsSortedCore)
|
||||||
Sort();
|
Sort();
|
||||||
else
|
else
|
||||||
//No user-defined sort is applied, so do default sorting by date added, descending
|
//No user sort is applied, so do default sorting by PurchaseDate, descending
|
||||||
((List<GridEntry>)Items).Sort((i1, i2) => i2.LibraryBook.DateAdded.CompareTo(i1.LibraryBook.DateAdded));
|
{
|
||||||
|
Comparer.PropertyName = nameof(GridEntry.DateAdded);
|
||||||
|
Comparer.Direction = ListSortDirection.Descending;
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||||
|
|
||||||
FilterString = null;
|
FilterString = null;
|
||||||
|
SearchResults = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Drawing;
|
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
|
namespace LibationWinForms
|
||||||
{
|
{
|
||||||
/// <summary>
|
public interface IHierarchical<T> where T : class
|
||||||
/// The View Model for a LibraryBook
|
|
||||||
/// </summary>
|
|
||||||
internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
|
|
||||||
{
|
{
|
||||||
#region implementation properties NOT exposed to the view
|
T Parent { get; }
|
||||||
// hide from public fields from Data Source GUI with [Browsable(false)]
|
List<T> Children { get; }
|
||||||
|
}
|
||||||
|
internal class LiberateStatus
|
||||||
|
{
|
||||||
|
public LiberatedStatus BookStatus;
|
||||||
|
public LiberatedStatus? PdfStatus;
|
||||||
|
public bool IsSeries;
|
||||||
|
public bool Expanded;
|
||||||
|
}
|
||||||
|
|
||||||
[Browsable(false)]
|
internal abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable, IHierarchical<GridEntry>
|
||||||
public string AudibleProductId => Book.AudibleProductId;
|
{
|
||||||
[Browsable(false)]
|
protected abstract Book Book { get; }
|
||||||
public LibraryBook LibraryBook { get; private set; }
|
|
||||||
[Browsable(false)]
|
|
||||||
public string LongDescription { get; private set; }
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Model properties exposed to the view
|
|
||||||
private Image _cover;
|
private Image _cover;
|
||||||
|
#region Model properties exposed to the view
|
||||||
private DateTime lastStatusUpdate = default;
|
|
||||||
private LiberatedStatus _bookStatus;
|
|
||||||
private LiberatedStatus? _pdfStatus;
|
|
||||||
public Image Cover
|
public Image Cover
|
||||||
{
|
{
|
||||||
get => _cover;
|
get => _cover;
|
||||||
private set
|
protected set
|
||||||
{
|
{
|
||||||
_cover = value;
|
_cover = value;
|
||||||
NotifyPropertyChanged();
|
NotifyPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public GridEntry Parent { get; set; }
|
||||||
public string ProductRating { get; private set; }
|
public List<GridEntry> Children { get; set; }
|
||||||
public string PurchaseDate { get; private set; }
|
public abstract string ProductRating { get; protected set; }
|
||||||
public string MyRating { get; private set; }
|
public abstract string PurchaseDate { get; protected set; }
|
||||||
public string Series { get; private set; }
|
public abstract DateTime DateAdded { get; }
|
||||||
public string Title { get; private set; }
|
public abstract string MyRating { get; protected set; }
|
||||||
public string Length { get; private set; }
|
public abstract string Series { get; protected set; }
|
||||||
public string Authors { get; private set; }
|
public abstract string Title { get; protected set; }
|
||||||
public string Narrators { get; private set; }
|
public abstract string Length { get; protected set; }
|
||||||
public string Category { get; private set; }
|
public abstract string Authors { get; protected set; }
|
||||||
public string Misc { get; private set; }
|
public abstract string Narrators { get; protected set; }
|
||||||
public string Description { get; private set; }
|
public abstract string Category { get; protected set; }
|
||||||
public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
public abstract string Misc { get; protected set; }
|
||||||
|
public abstract string Description { get; protected set; }
|
||||||
// these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint
|
public abstract string DisplayTags { get; }
|
||||||
public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
|
public abstract LiberateStatus Liberate { get; }
|
||||||
{
|
public abstract object GetMemberValue(string memberName);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
#endregion
|
||||||
|
public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
|
||||||
|
|
||||||
// alias
|
protected void LoadCover()
|
||||||
private Book Book => LibraryBook.Book;
|
|
||||||
|
|
||||||
public GridEntry(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();
|
|
||||||
|
|
||||||
// Get cover art. If it's default, subscribe to PictureCached
|
// Get cover art. If it's default, subscribe to PictureCached
|
||||||
{
|
{
|
||||||
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
(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.
|
// Mutable property. Set the field so PropertyChanged isn't fired.
|
||||||
_cover = ImageReader.ToImage(picture);
|
_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)
|
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.
|
// Instantiate comparers for every exposed member object type.
|
||||||
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
|
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
|
||||||
{
|
{
|
||||||
{ typeof(string), new ObjectComparer<string>() },
|
{ typeof(string), new ObjectComparer<string>() },
|
||||||
{ typeof(int), new ObjectComparer<int>() },
|
{ typeof(int), new ObjectComparer<int>() },
|
||||||
{ typeof(float), new ObjectComparer<float>() },
|
{ typeof(float), new ObjectComparer<float>() },
|
||||||
|
{ typeof(bool), new ObjectComparer<bool>() },
|
||||||
{ typeof(DateTime), new ObjectComparer<DateTime>() },
|
{ typeof(DateTime), new ObjectComparer<DateTime>() },
|
||||||
{ typeof(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
|
{ 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()
|
~GridEntry()
|
||||||
{
|
{
|
||||||
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
|
|
||||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,15 +20,27 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
253
Source/LibationWinForms/grid/LibraryBookEntry.cs
Normal file
253
Source/LibationWinForms/grid/LibraryBookEntry.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Source/LibationWinForms/grid/MasterDataGridView.cs
Normal file
26
Source/LibationWinForms/grid/MasterDataGridView.cs
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Source/LibationWinForms/grid/ProductsDisplay.Designer.cs
generated
Normal file
46
Source/LibationWinForms/grid/ProductsDisplay.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
152
Source/LibationWinForms/grid/ProductsDisplay.cs
Normal file
152
Source/LibationWinForms/grid/ProductsDisplay.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Source/LibationWinForms/grid/ProductsDisplay.resx
Normal file
63
Source/LibationWinForms/grid/ProductsDisplay.resx
Normal 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>
|
||||||
@ -64,20 +64,20 @@
|
|||||||
this.gridEntryDataGridView.AutoGenerateColumns = false;
|
this.gridEntryDataGridView.AutoGenerateColumns = false;
|
||||||
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||||
this.liberateGVColumn,
|
this.liberateGVColumn,
|
||||||
this.coverGVColumn,
|
this.coverGVColumn,
|
||||||
this.titleGVColumn,
|
this.titleGVColumn,
|
||||||
this.authorsGVColumn,
|
this.authorsGVColumn,
|
||||||
this.narratorsGVColumn,
|
this.narratorsGVColumn,
|
||||||
this.lengthGVColumn,
|
this.lengthGVColumn,
|
||||||
this.seriesGVColumn,
|
this.seriesGVColumn,
|
||||||
this.descriptionGVColumn,
|
this.descriptionGVColumn,
|
||||||
this.categoryGVColumn,
|
this.categoryGVColumn,
|
||||||
this.productRatingGVColumn,
|
this.productRatingGVColumn,
|
||||||
this.purchaseDateGVColumn,
|
this.purchaseDateGVColumn,
|
||||||
this.myRatingGVColumn,
|
this.myRatingGVColumn,
|
||||||
this.miscGVColumn,
|
this.miscGVColumn,
|
||||||
this.tagAndDetailsGVColumn});
|
this.tagAndDetailsGVColumn});
|
||||||
this.gridEntryDataGridView.ContextMenuStrip = this.contextMenuStrip1;
|
this.gridEntryDataGridView.ContextMenuStrip = this.contextMenuStrip1;
|
||||||
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
|
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
|
||||||
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||||
|
|||||||
@ -2,14 +2,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core.Windows.Forms;
|
using Dinah.Core.Windows.Forms;
|
||||||
using FileLiberator;
|
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationWinForms.Dialogs;
|
|
||||||
|
|
||||||
namespace LibationWinForms
|
namespace LibationWinForms
|
||||||
{
|
{
|
||||||
@ -36,7 +33,17 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
public partial class ProductsGrid : UserControl
|
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>
|
/// <summary>Number of visible rows has changed</summary>
|
||||||
public event EventHandler<int> VisibleCountChanged;
|
public event EventHandler<int> VisibleCountChanged;
|
||||||
|
|
||||||
@ -53,8 +60,14 @@ namespace LibationWinForms
|
|||||||
EnableDoubleBuffering();
|
EnableDoubleBuffering();
|
||||||
|
|
||||||
_dataGridView.CellContentClick += DataGridView_CellContentClick;
|
_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()
|
private void EnableDoubleBuffering()
|
||||||
@ -66,117 +79,70 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
#region Button controls
|
#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
|
// handle grid button click: https://stackoverflow.com/a/13687844
|
||||||
if (e.RowIndex < 0)
|
if (e.RowIndex < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (e.ColumnIndex == liberateGVColumn.Index)
|
var entry = getGridEntry(e.RowIndex);
|
||||||
Liberate_Click(getGridEntry(e.RowIndex));
|
if (entry is LibraryBookEntry lbEntry)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
imageDisplay = new ImageDisplay();
|
if (e.ColumnIndex == liberateGVColumn.Index)
|
||||||
imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
|
LiberateClicked?.Invoke(e, lbEntry);
|
||||||
imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
|
else if (e.ColumnIndex == tagAndDetailsGVColumn.Index && entry is LibraryBookEntry)
|
||||||
imageDisplay.Show(this);
|
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);
|
||||||
}
|
}
|
||||||
|
else if (entry is SeriesEntry sEntry && e.ColumnIndex == liberateGVColumn.Index)
|
||||||
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
|
|
||||||
{
|
{
|
||||||
SpawnLocation = PointToScreen(cellDisplay.Location + new Size(cellDisplay.Width, 0)),
|
if (sEntry.Liberate.Expanded)
|
||||||
DescriptionText = liveGridEntry.LongDescription,
|
bindingList.CollapseItem(sEntry);
|
||||||
BorderThickness = 2,
|
else
|
||||||
};
|
bindingList.ExpandItem(sEntry);
|
||||||
|
|
||||||
void CloseWindow(object o, EventArgs e)
|
sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate));
|
||||||
{
|
|
||||||
displayWindow.Close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_dataGridView.Scroll += CloseWindow;
|
|
||||||
displayWindow.FormClosed += (_, _) => _dataGridView.Scroll -= CloseWindow;
|
|
||||||
displayWindow.Show(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Liberate_Click(GridEntry liveGridEntry)
|
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region UI display functions
|
#region UI display functions
|
||||||
|
|
||||||
private FilterableSortableBindingList bindingList;
|
internal void bindToGrid(List<LibraryBook> dbBooks)
|
||||||
|
|
||||||
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 geList = dbBooks.Where(b => b.Book.ContentType is not ContentType.Episode).Select(b => new LibraryBookEntry(b)).Cast<GridEntry>().ToList();
|
||||||
var lib = DbContexts.GetLibrary_Flat_NoTracking();
|
|
||||||
|
|
||||||
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
|
var seriesEntry = new SeriesEntry();
|
||||||
bindToGrid(lib);
|
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();
|
||||||
hasBeenDisplayed = true;
|
|
||||||
InitialLoaded?.Invoke(this, new());
|
seriesEntry.setSeriesBook(s);
|
||||||
VisibleCountChanged?.Invoke(this, bindingList.Count);
|
geList.Add(seriesEntry);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
updateGrid(lib);
|
|
||||||
|
|
||||||
}
|
bindingList = new FilterableSortableBindingList(geList.OrderByDescending(ge => ge.DateAdded));
|
||||||
|
|
||||||
private void bindToGrid(List<LibraryBook> dbBooks)
|
|
||||||
{
|
|
||||||
bindingList = new FilterableSortableBindingList(dbBooks.OrderByDescending(lb => lb.DateAdded).Select(lb => new GridEntry(lb)));
|
|
||||||
gridEntryBindingSource.DataSource = bindingList;
|
gridEntryBindingSource.DataSource = bindingList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateGrid(List<LibraryBook> dbBooks)
|
internal void updateGrid(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
int visibleCount = bindingList.Count;
|
int visibleCount = bindingList.Count;
|
||||||
string existingFilter = gridEntryBindingSource.Filter;
|
string existingFilter = gridEntryBindingSource.Filter;
|
||||||
|
|
||||||
//Add absent books to grid, or update current books
|
//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--)
|
for (var i = dbBooks.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var libraryBook = dbBooks[i];
|
var libraryBook = dbBooks[i];
|
||||||
@ -184,10 +150,37 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
// add new to top
|
// add new to top
|
||||||
if (existingItem is null)
|
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
|
// update existing
|
||||||
else
|
else
|
||||||
|
{
|
||||||
existingItem.UpdateLibraryBook(libraryBook);
|
existingItem.UpdateLibraryBook(libraryBook);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bindingList.Count != visibleCount)
|
if (bindingList.Count != visibleCount)
|
||||||
@ -199,13 +192,22 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
// remove deleted from grid.
|
// remove deleted from grid.
|
||||||
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
||||||
var removedBooks =
|
var removedBooks =
|
||||||
bindingList
|
bindingList
|
||||||
.AllItems()
|
.AllItems()
|
||||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId)
|
.Where(i => i is LibraryBookEntry)
|
||||||
.ToList();
|
.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
|
//no need to re-filter for removed books
|
||||||
bindingList.Remove(removed);
|
bindingList.Remove(removed);
|
||||||
|
|
||||||
@ -232,12 +234,11 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
internal List<LibraryBook> GetVisible()
|
internal IEnumerable<LibraryBook> GetVisible()
|
||||||
=> bindingList
|
=> bindingList
|
||||||
.Select(row => row.LibraryBook)
|
.Where(row => row is LibraryBookEntry)
|
||||||
.ToList();
|
.Cast<LibraryBookEntry>()
|
||||||
|
.Select(row => row.LibraryBook);
|
||||||
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
|
|
||||||
|
|
||||||
#region Column Customizations
|
#region Column Customizations
|
||||||
|
|
||||||
@ -293,8 +294,6 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index);
|
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnVisibleChanged(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
|
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
|
||||||
|
|||||||
@ -57,12 +57,6 @@
|
|||||||
<resheader name="writer">
|
<resheader name="writer">
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
</resheader>
|
</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">
|
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||||
<value>81</value>
|
<value>81</value>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
|||||||
90
Source/LibationWinForms/grid/SeriesEntry.cs
Normal file
90
Source/LibationWinForms/grid/SeriesEntry.cs
Normal 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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
111
Source/LibationWinForms/grid/SortableBindingList1.cs
Normal file
111
Source/LibationWinForms/grid/SortableBindingList1.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user