Merge pull request #579 from Mbucari/master
Bug fixes and more shared code moved to UI base
This commit is contained in:
commit
1a95f2923b
@ -23,7 +23,7 @@ namespace LibationAvalonia.ViewModels
|
||||
/// <summary>Backing list of all grid entries</summary>
|
||||
private readonly AvaloniaList<IGridEntry> SOURCE = new();
|
||||
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
||||
private List<IGridEntry> FilteredInGridEntries;
|
||||
private HashSet<IGridEntry> FilteredInGridEntries;
|
||||
public string FilterString { get; private set; }
|
||||
public DataGridCollectionView GridEntries { get; private set; }
|
||||
|
||||
@ -117,8 +117,8 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
//Create the filtered-in list before adding entries to avoid a refresh
|
||||
FilteredInGridEntries = QueryResults(geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)), FilterString);
|
||||
SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded));
|
||||
FilteredInGridEntries = geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||
SOURCE.AddRange(geList.OrderDescending(new RowComparer(null)));
|
||||
|
||||
//Add all children beneath their parent
|
||||
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
|
||||
@ -301,7 +301,7 @@ namespace LibationAvalonia.ViewModels
|
||||
if (SOURCE.Count == 0)
|
||||
return;
|
||||
|
||||
FilteredInGridEntries = QueryResults(SOURCE, searchString);
|
||||
FilteredInGridEntries = SOURCE.FilterEntries(searchString);
|
||||
|
||||
await refreshGrid();
|
||||
}
|
||||
@ -318,25 +318,11 @@ namespace LibationAvalonia.ViewModels
|
||||
return FilteredInGridEntries.Contains(item);
|
||||
}
|
||||
|
||||
private static List<IGridEntry> QueryResults(IEnumerable<IGridEntry> entries, string searchString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString)) return null;
|
||||
|
||||
var searchResultSet = SearchEngineCommands.Search(searchString);
|
||||
|
||||
var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||
|
||||
return booksFilteredIn.Concat(seriesFilteredIn).ToList();
|
||||
}
|
||||
|
||||
private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var filterResults = QueryResults(SOURCE, FilterString);
|
||||
var filterResults = SOURCE.FilterEntries(FilterString);
|
||||
|
||||
if (filterResults is not null && FilteredInGridEntries.Intersect(filterResults).Count() != FilteredInGridEntries.Count)
|
||||
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||
{
|
||||
FilteredInGridEntries = filterResults;
|
||||
await refreshGrid();
|
||||
|
||||
@ -1,98 +1,28 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationUiBase.GridView;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// This compare class ensures that all top-level grid entries (standalone books or series parents)
|
||||
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
|
||||
/// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
|
||||
/// properties when 2 items compare equal.
|
||||
/// </summary>
|
||||
internal class RowComparer : IComparer, IComparer<IGridEntry>, IComparer<object>
|
||||
internal class RowComparer : RowComparerBase
|
||||
{
|
||||
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
public DataGridColumn Column { get; init; }
|
||||
public string PropertyName { get; private set; }
|
||||
private DataGridColumn Column { get; init; }
|
||||
public override string PropertyName { get; set; }
|
||||
|
||||
public RowComparer(DataGridColumn column)
|
||||
{
|
||||
Column = column;
|
||||
PropertyName = Column.SortMemberPath;
|
||||
}
|
||||
|
||||
public int Compare(object x, object y)
|
||||
{
|
||||
if (x is null && y is not null) return -1;
|
||||
if (x is not null && y is null) return 1;
|
||||
if (x is null && y is null) return 0;
|
||||
|
||||
var geA = (IGridEntry)x;
|
||||
var geB = (IGridEntry)y;
|
||||
|
||||
var sortDirection = GetSortOrder();
|
||||
|
||||
ISeriesEntry parentA = null;
|
||||
ISeriesEntry parentB = null;
|
||||
|
||||
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
||||
parentA = seA;
|
||||
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
||||
parentB = seB;
|
||||
|
||||
//both a and b are top-level grid entries
|
||||
if (parentA is null && parentB is null)
|
||||
return InternalCompare(geA, geB);
|
||||
|
||||
//a is top-level, b is a child
|
||||
if (parentA is null && parentB is not null)
|
||||
{
|
||||
// b is a child of a, parent is always first
|
||||
if (parentB == geA)
|
||||
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
||||
else
|
||||
return InternalCompare(geA, parentB);
|
||||
}
|
||||
|
||||
//a is a child, b is a top-level
|
||||
if (parentA is not null && parentB is null)
|
||||
{
|
||||
// a is a child of b, parent is always first
|
||||
if (parentA == geB)
|
||||
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
||||
else
|
||||
return InternalCompare(parentA, geB);
|
||||
}
|
||||
|
||||
//both are children of the same series
|
||||
if (parentA == parentB)
|
||||
return InternalCompare(geA, geB);
|
||||
|
||||
//a and b are children of different series.
|
||||
return InternalCompare(parentA, parentB);
|
||||
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
|
||||
}
|
||||
|
||||
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
||||
private ListSortDirection? GetSortOrder()
|
||||
=> CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?;
|
||||
|
||||
private int InternalCompare(IGridEntry x, IGridEntry y)
|
||||
{
|
||||
var val1 = x.GetMemberValue(PropertyName);
|
||||
var val2 = y.GetMemberValue(PropertyName);
|
||||
|
||||
return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); ;
|
||||
}
|
||||
|
||||
public int Compare(IGridEntry x, IGridEntry y)
|
||||
{
|
||||
return Compare((object)x, y);
|
||||
}
|
||||
protected override ListSortDirection GetSortOrder()
|
||||
=> Column is null ? ListSortDirection.Descending
|
||||
: CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) is ListSortDirection lsd ? lsd
|
||||
: ListSortDirection.Descending;
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ namespace LibationUiBase.GridView
|
||||
public abstract object BackgroundBrush { get; }
|
||||
public object ButtonImage => GetLiberateIcon();
|
||||
public string ToolTip => GetTooltip();
|
||||
protected internal Book Book { get; internal set; }
|
||||
private Book Book { get; }
|
||||
|
||||
private DateTime lastBookUpdate;
|
||||
private LiberatedStatus bookStatus;
|
||||
|
||||
@ -154,9 +154,13 @@ namespace LibationUiBase.GridView
|
||||
if (udi.Book.AudibleProductId != Book.AudibleProductId)
|
||||
return;
|
||||
|
||||
if (udi.Book != LibraryBook.Book)
|
||||
{
|
||||
//If UserDefinedItem was changed on a different Book instance (such as when batch liberating via menus),
|
||||
//EntryStatu's Book instance will not have the current DB state.
|
||||
Liberate.Book = udi.Book;
|
||||
//Liberate.Book and LibraryBook.Book instances will not have the current DB state.
|
||||
Invoke(() => UpdateLibraryBook(new LibraryBook(udi.Book, LibraryBook.DateAdded, LibraryBook.Account)));
|
||||
return;
|
||||
}
|
||||
|
||||
// UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
|
||||
// - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using DataLayer;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -39,6 +40,26 @@ namespace LibationUiBase.GridView
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool SearchSetsDiffer(this HashSet<IGridEntry>? searchSet, HashSet<IGridEntry>? otherSet)
|
||||
=> searchSet is null != otherSet is null ||
|
||||
(searchSet is not null &&
|
||||
otherSet is not null &&
|
||||
searchSet.Intersect(otherSet).Count() != searchSet.Count);
|
||||
|
||||
public static HashSet<IGridEntry>? FilterEntries(this IEnumerable<IGridEntry> entries, string searchString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString)) return null;
|
||||
|
||||
var searchResultSet = SearchEngineCommands.Search(searchString);
|
||||
|
||||
var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||
|
||||
return booksFilteredIn.Concat(seriesFilteredIn).ToHashSet();
|
||||
}
|
||||
}
|
||||
#nullable disable
|
||||
}
|
||||
|
||||
91
Source/LibationUiBase/GridView/RowComparerBase.cs
Normal file
91
Source/LibationUiBase/GridView/RowComparerBase.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>
|
||||
/// This compare class ensures that all top-level grid entries (standalone books or series parents)
|
||||
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
|
||||
/// sorted by series index, ascending.
|
||||
/// </summary>
|
||||
public abstract class RowComparerBase : IComparer, IComparer<IGridEntry>, IComparer<object>
|
||||
{
|
||||
public abstract string PropertyName { get; set; }
|
||||
|
||||
public int Compare(object x, object y)
|
||||
{
|
||||
if (x is null && y is not null) return -1;
|
||||
if (x is not null && y is null) return 1;
|
||||
if (x is null && y is null) return 0;
|
||||
|
||||
var geA = (IGridEntry)x;
|
||||
var geB = (IGridEntry)y;
|
||||
|
||||
var sortDirection = GetSortOrder();
|
||||
|
||||
ISeriesEntry parentA = null;
|
||||
ISeriesEntry parentB = null;
|
||||
|
||||
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
||||
parentA = seA;
|
||||
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
||||
parentB = seB;
|
||||
|
||||
//both a and b are top-level grid entries
|
||||
if (parentA is null && parentB is null)
|
||||
return InternalCompare(geA, geB);
|
||||
|
||||
//a is top-level, b is a child
|
||||
if (parentA is null && parentB is not null)
|
||||
{
|
||||
// b is a child of a, parent is always first
|
||||
if (parentB == geA)
|
||||
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
||||
else
|
||||
return InternalCompare(geA, parentB);
|
||||
}
|
||||
|
||||
//a is a child, b is a top-level
|
||||
if (parentA is not null && parentB is null)
|
||||
{
|
||||
// a is a child of b, parent is always first
|
||||
if (parentA == geB)
|
||||
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
||||
else
|
||||
return InternalCompare(parentA, geB);
|
||||
}
|
||||
|
||||
//both are children of the same series
|
||||
if (parentA == parentB)
|
||||
{
|
||||
//Podcast episodes usually all have the same PurchaseDate and DateAdded property:
|
||||
//the date that the series was added to the library. So when sorting by PurchaseDate
|
||||
//and DateAdded, compare SeriesOrder instead..
|
||||
return PropertyName switch
|
||||
{
|
||||
nameof(IGridEntry.DateAdded) or nameof (IGridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
|
||||
_ => InternalCompare(geA, geB),
|
||||
};
|
||||
}
|
||||
|
||||
//a and b are children of different series.
|
||||
return InternalCompare(parentA, parentB);
|
||||
}
|
||||
|
||||
protected abstract ListSortDirection GetSortOrder();
|
||||
|
||||
private int InternalCompare(IGridEntry x, IGridEntry y)
|
||||
{
|
||||
var val1 = x.GetMemberValue(PropertyName);
|
||||
var val2 = y.GetMemberValue(PropertyName);
|
||||
|
||||
return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); ;
|
||||
}
|
||||
|
||||
public int Compare(IGridEntry x, IGridEntry y)
|
||||
{
|
||||
return Compare((object)x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,7 +47,7 @@ namespace LibationUiBase.GridView
|
||||
|
||||
Children = children
|
||||
.Select(c => new LibraryBookEntry<TStatus>(c, this))
|
||||
.OrderBy(c => c.SeriesIndex)
|
||||
.OrderByDescending(c => c.SeriesOrder)
|
||||
.ToList<ILibraryBookEntry>();
|
||||
|
||||
UpdateLibraryBook(parent);
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using ApplicationServices;
|
||||
using Dinah.Core.DataBinding;
|
||||
using LibationSearchEngine;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -20,51 +18,62 @@ namespace LibationWinForms.GridView
|
||||
*
|
||||
* Remove is overridden to ensure that removed items are removed from
|
||||
* the base list (visible items) as well as the FilterRemoved list.
|
||||
*
|
||||
* Using BindingList.Add/Insert and BindingList.Remove will cause the
|
||||
* BindingList to subscribe/unsibscribe to/from the item's PropertyChanged
|
||||
* event. Adding or removing from the underlying list will not change the
|
||||
* BindingList's subscription to that item.
|
||||
*/
|
||||
internal class GridEntryBindingList : BindingList<IGridEntry>, IBindingListView
|
||||
{
|
||||
public GridEntryBindingList() : base(new List<IGridEntry>()) { }
|
||||
public GridEntryBindingList(IEnumerable<IGridEntry> enumeration) : base(new List<IGridEntry>(enumeration))
|
||||
{
|
||||
SearchEngineCommands.SearchEngineUpdated += (_,_) => ApplyFilter(FilterString);
|
||||
SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
|
||||
ListChanged += GridEntryBindingList_ListChanged;
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
|
||||
/// <returns>All items in the list, including those filtered out.</returns>
|
||||
public List<IGridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
|
||||
|
||||
/// <summary>All items that pass the current filter</summary>
|
||||
public IEnumerable<ILibraryBookEntry> GetFilteredInItems()
|
||||
=> SearchResults is null
|
||||
? FilterRemoved
|
||||
=> FilteredInGridEntries?
|
||||
.OfType<ILibraryBookEntry>()
|
||||
.Union(Items.OfType<ILibraryBookEntry>())
|
||||
: FilterRemoved
|
||||
?? FilterRemoved
|
||||
.OfType<ILibraryBookEntry>()
|
||||
.Join(SearchResults.Docs, o => o.Book.AudibleProductId, i => i.ProductId, (o, _) => o)
|
||||
.Union(Items.OfType<ILibraryBookEntry>());
|
||||
|
||||
public bool SupportsFiltering => true;
|
||||
public string Filter { get => FilterString; set => ApplyFilter(value); }
|
||||
public string Filter
|
||||
{
|
||||
get => FilterString;
|
||||
set
|
||||
{
|
||||
FilterString = value;
|
||||
|
||||
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary>
|
||||
public bool SuspendFilteringOnUpdate { get; set; }
|
||||
if (Items.Count + FilterRemoved.Count == 0)
|
||||
return;
|
||||
|
||||
protected MemberComparer<IGridEntry> Comparer { get; } = new();
|
||||
FilteredInGridEntries = AllItems().FilterEntries(FilterString);
|
||||
refreshEntries();
|
||||
}
|
||||
}
|
||||
|
||||
protected RowComparer 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 ListSortDirection SortDirectionCore => Comparer.SortOrder;
|
||||
|
||||
/// <summary> Items that were removed from the base list due to filtering </summary>
|
||||
private readonly List<IGridEntry> FilterRemoved = new();
|
||||
private string FilterString;
|
||||
private SearchResultSet SearchResults;
|
||||
private bool isSorted;
|
||||
private ListSortDirection listSortDirection;
|
||||
private PropertyDescriptor propertyDescriptor;
|
||||
|
||||
/// <summary> All GridEntries present in the current filter set. If null, no filter is applied and all entries are filtered in.(This was a performance choice)</summary>
|
||||
private HashSet<IGridEntry> FilteredInGridEntries;
|
||||
|
||||
#region Unused - Advanced Filtering
|
||||
public bool SupportsAdvancedSorting => false;
|
||||
@ -82,25 +91,57 @@ namespace LibationWinForms.GridView
|
||||
base.Remove(entry);
|
||||
}
|
||||
|
||||
private void ApplyFilter(string filterString)
|
||||
/// <summary>
|
||||
/// This method should be called whenever there's been a change to the
|
||||
/// set of all GridEntries that affects sort order or filter status
|
||||
/// </summary>
|
||||
private void refreshEntries()
|
||||
{
|
||||
if (filterString != FilterString)
|
||||
RemoveFilter();
|
||||
|
||||
FilterString = filterString;
|
||||
SearchResults = SearchEngineCommands.Search(filterString);
|
||||
|
||||
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||
|
||||
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
|
||||
|
||||
foreach (var item in filteredOut)
|
||||
if (FilteredInGridEntries is null)
|
||||
{
|
||||
FilterRemoved.Add(item);
|
||||
base.Remove(item);
|
||||
addRemovedItemsBack(FilterRemoved.ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
var addBackEntries = FilterRemoved.Intersect(FilteredInGridEntries).ToList();
|
||||
var toRemoveEntries = Items.Except(FilteredInGridEntries).ToList();
|
||||
|
||||
addRemovedItemsBack(addBackEntries);
|
||||
|
||||
foreach (var newRemove in toRemoveEntries)
|
||||
{
|
||||
FilterRemoved.Add(newRemove);
|
||||
base.Remove(newRemove);
|
||||
}
|
||||
}
|
||||
|
||||
SortInternal();
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
|
||||
void addRemovedItemsBack(List<IGridEntry> addBackEntries)
|
||||
{
|
||||
//Add removed entries back into Items so they are displayed
|
||||
//(except for episodes that are collapsed)
|
||||
foreach (var addBack in addBackEntries)
|
||||
{
|
||||
if (addBack is ILibraryBookEntry lbe && lbe.Parent is ISeriesEntry se && !se.Liberate.Expanded)
|
||||
continue;
|
||||
|
||||
FilterRemoved.Remove(addBack);
|
||||
Add(addBack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var filterResults = AllItems().FilterEntries(FilterString);
|
||||
|
||||
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||
{
|
||||
FilteredInGridEntries = filterResults;
|
||||
refreshEntries();
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,7 +159,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
public void CollapseItem(ISeriesEntry sEntry)
|
||||
{
|
||||
foreach (var episode in sEntry.Children.Join(Items.BookEntries(), o => o, i => i, (_, i) => i).ToList())
|
||||
foreach (var episode in sEntry.Children.Intersect(Items.BookEntries()).ToList())
|
||||
{
|
||||
FilterRemoved.Add(episode);
|
||||
base.Remove(episode);
|
||||
@ -131,9 +172,9 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
var sindex = Items.IndexOf(sEntry);
|
||||
|
||||
foreach (var episode in sEntry.Children.Join(FilterRemoved.BookEntries(), o => o, i => i, (_, i) => i).ToList())
|
||||
foreach (var episode in Comparer.OrderEntries(sEntry.Children.Intersect(FilterRemoved.BookEntries())).ToList())
|
||||
{
|
||||
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
||||
if (FilteredInGridEntries?.Contains(episode) ?? true)
|
||||
{
|
||||
FilterRemoved.Remove(episode);
|
||||
InsertItem(++sindex, episode);
|
||||
@ -144,106 +185,45 @@ namespace LibationWinForms.GridView
|
||||
|
||||
public void RemoveFilter()
|
||||
{
|
||||
if (FilterString is null) return;
|
||||
|
||||
int visibleCount = Items.Count;
|
||||
|
||||
foreach (var item in FilterRemoved.ToList())
|
||||
{
|
||||
if (item is ISeriesEntry || (item is ILibraryBookEntry lbe && (lbe.Liberate.IsBook || lbe.Parent.Liberate.Expanded)))
|
||||
{
|
||||
FilterRemoved.Remove(item);
|
||||
InsertItem(visibleCount++, item);
|
||||
}
|
||||
}
|
||||
|
||||
if (IsSortedCore)
|
||||
Sort();
|
||||
else
|
||||
//No user sort is applied, so do default sorting by DateAdded, descending
|
||||
{
|
||||
Comparer.PropertyName = nameof(IGridEntry.DateAdded);
|
||||
Comparer.Direction = ListSortDirection.Descending;
|
||||
Sort();
|
||||
}
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
|
||||
FilterString = null;
|
||||
SearchResults = null;
|
||||
FilteredInGridEntries = null;
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
|
||||
{
|
||||
Comparer.PropertyName = property.Name;
|
||||
Comparer.Direction = direction;
|
||||
Comparer.SortOrder = direction;
|
||||
|
||||
Sort();
|
||||
SortInternal();
|
||||
|
||||
propertyDescriptor = property;
|
||||
listSortDirection = direction;
|
||||
isSorted = true;
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
}
|
||||
|
||||
protected void Sort()
|
||||
private void SortInternal()
|
||||
{
|
||||
var itemsList = (List<IGridEntry>)Items;
|
||||
|
||||
var children = itemsList.BookEntries().Where(i => i.Liberate.IsEpisode).ToList();
|
||||
|
||||
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
|
||||
//User Order/OrderDescending and replace items in list instead of using List.Sort() to achieve stable sorting.
|
||||
var sortedItems = Comparer.OrderEntries(itemsList).ToList();
|
||||
|
||||
itemsList.Clear();
|
||||
|
||||
//Only add parentless items at this stage. After these items are added in the
|
||||
//correct sorting order, go back and add the children beneath their parents.
|
||||
itemsList.AddRange(sortedItems);
|
||||
|
||||
foreach (var parent in children.Select(c => c.Parent).Distinct())
|
||||
{
|
||||
var pIndex = itemsList.IndexOf(parent);
|
||||
|
||||
//children are sorted beneath their series parent
|
||||
foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c, Comparer))
|
||||
itemsList.Insert(++pIndex, c);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnListChanged(ListChangedEventArgs e)
|
||||
private void GridEntryBindingList_ListChanged(object sender, ListChangedEventArgs e)
|
||||
{
|
||||
if (e.ListChangedType == ListChangedType.ItemChanged)
|
||||
{
|
||||
if (FilterString is not null && !SuspendFilteringOnUpdate && Items[e.NewIndex] is ILibraryBookEntry lbItem)
|
||||
{
|
||||
SearchResults = SearchEngineCommands.Search(FilterString);
|
||||
if (!SearchResults.Docs.Any(d => d.ProductId == lbItem.AudibleProductId))
|
||||
{
|
||||
FilterRemoved.Add(lbItem);
|
||||
base.Remove(lbItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSorted && e.PropertyDescriptor == SortPropertyCore)
|
||||
{
|
||||
var item = Items[e.NewIndex];
|
||||
Sort();
|
||||
var newIndex = Items.IndexOf(item);
|
||||
|
||||
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemMoved, newIndex, e.NewIndex));
|
||||
return;
|
||||
}
|
||||
}
|
||||
base.OnListChanged(e);
|
||||
if (e.ListChangedType == ListChangedType.ItemChanged && IsSortedCore && e.PropertyDescriptor == SortPropertyCore)
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
protected override void RemoveSortCore()
|
||||
{
|
||||
isSorted = false;
|
||||
propertyDescriptor = base.SortPropertyCore;
|
||||
listSortDirection = base.SortDirectionCore;
|
||||
Comparer.SortOrder = base.SortDirectionCore;
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
}
|
||||
|
||||
@ -181,7 +181,7 @@ namespace LibationWinForms.GridView
|
||||
geList.AddRange(seriesEntry.Children);
|
||||
}
|
||||
|
||||
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
|
||||
bindingList = new GridEntryBindingList(geList);
|
||||
bindingList.CollapseAll();
|
||||
syncBindingSource.DataSource = bindingList;
|
||||
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
|
||||
@ -195,8 +195,6 @@ namespace LibationWinForms.GridView
|
||||
string existingFilter = syncBindingSource.Filter;
|
||||
Filter(null);
|
||||
|
||||
bindingList.SuspendFilteringOnUpdate = true;
|
||||
|
||||
//Add absent entries to grid, or update existing entry
|
||||
|
||||
var allEntries = bindingList.AllItems().BookEntries();
|
||||
@ -219,8 +217,6 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
}
|
||||
|
||||
bindingList.SuspendFilteringOnUpdate = false;
|
||||
|
||||
//Re-apply filter after adding new/updating existing books to capture any changes
|
||||
Filter(existingFilter);
|
||||
|
||||
|
||||
20
Source/LibationWinForms/GridView/RowComparer.cs
Normal file
20
Source/LibationWinForms/GridView/RowComparer.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using LibationUiBase.GridView;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
internal class RowComparer : RowComparerBase
|
||||
{
|
||||
public ListSortDirection SortOrder { get; set; } = ListSortDirection.Descending;
|
||||
public override string PropertyName { get; set; } = nameof(IGridEntry.DateAdded);
|
||||
protected override ListSortDirection GetSortOrder() => SortOrder;
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for ordering grid entries
|
||||
/// </summary>
|
||||
public IOrderedEnumerable<IGridEntry> OrderEntries(IEnumerable<IGridEntry> entries)
|
||||
=> SortOrder is ListSortDirection.Descending ? entries.OrderDescending(this) : entries.Order(this);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user