Fix sorting and refactor

This commit is contained in:
Michael Bucari-Tovo 2022-07-13 11:48:17 -06:00
parent e33fd6ea1b
commit 3a61c32881
20 changed files with 649 additions and 748 deletions

View File

@ -1,79 +1,17 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Reflection;
namespace LibationWinForms.AvaloniaUI.Controls
{
/// <summary> The purpose of this extension it to immediately commit any check
/// state changes to the viewmodel. There must be a better way to do this, but
/// I sure as shit can't find it. </summary>
public partial class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
{
Func<DataGrid> _owningGrid_get;
Func<DataGridEditAction, bool, bool, bool, bool> _endCellEdit;
Func<Action, bool> _waitForLostFocus;
public DataGrid OwningGrid
{
get
{
if (_owningGrid_get == null)
{
var pi = typeof(DataGridColumn).GetProperty(nameof(OwningGrid), BindingFlags.NonPublic | BindingFlags.Instance);
var mi = pi.GetGetMethod(true);
_owningGrid_get = mi.CreateDelegate<Func<DataGrid>>(this);
}
return _owningGrid_get();
}
}
public Func<Action, bool> WaitForLostFocus
{
get
{
if (_endCellEdit == null)
{
var mi = typeof(DataGrid).GetMethod(nameof(WaitForLostFocus), BindingFlags.NonPublic | BindingFlags.Instance);
_waitForLostFocus = mi.CreateDelegate<Func<Action, bool>>(OwningGrid);
}
return _waitForLostFocus;
}
}
public Func<DataGridEditAction, bool, bool, bool, bool> EndCellEdit
{
get
{
if (_endCellEdit == null)
{
var mi = typeof(DataGrid).GetMethod(nameof(EndCellEdit), BindingFlags.NonPublic | BindingFlags.Instance);
_endCellEdit = mi.CreateDelegate<Func<DataGridEditAction, bool, bool, bool, bool>>(OwningGrid);
}
return _endCellEdit;
}
}
protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
{
//Only SeriesEntry types have three-state checks, individual LibraryEntry books are binary.
var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox;
ele.Checked += EditingElement_Checked;
ele.Unchecked += EditingElement_Checked;
ele.Indeterminate += EditingElement_Checked;
ele.IsThreeState = dataItem is SeriesEntrys2;
return ele;
}
private void EditingElement_Checked(object sender, RoutedEventArgs e)
{
if (sender is CheckBox cbox && cbox.DataContext is GridEntry2 gentry)
{
var check = cbox.IsChecked;
WaitForLostFocus(() =>
{
EndCellEdit(DataGridEditAction.Cancel, true, true, false);
gentry.Remove = check;
});
}
}
}
}

View File

@ -1,4 +1,5 @@
using DataLayer;
using Avalonia.Media;
using DataLayer;
using Dinah.Core;
using Dinah.Core.DataBinding;
using Dinah.Core.Drawing;
@ -28,39 +29,32 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
[Browsable(false)] public float SeriesIndex { get; protected set; }
[Browsable(false)] public string LongDescription { get; protected set; }
[Browsable(false)] public abstract DateTime DateAdded { get; }
[Browsable(false)] public int ListIndex { get; set; }
[Browsable(false)] protected Book Book => LibraryBook.Book;
#region Model properties exposed to the view
private Avalonia.Media.Imaging.Bitmap _cover;
private string _purchaseDate;
private string _series;
private string _title;
private string _length;
private string _authors;
private string _narrators;
private string _category;
private string _misc;
private string _description;
private string _productRating;
private string _myRating;
public Avalonia.Media.Imaging.Bitmap Cover { get => _cover; protected set { this.RaiseAndSetIfChanged(ref _cover, value); } }
public string PurchaseDate { get => _purchaseDate; protected set { this.RaiseAndSetIfChanged(ref _purchaseDate, value); } }
public string Series { get => _series; protected set { this.RaiseAndSetIfChanged(ref _series, value); } }
public string Title { get => _title; protected set { this.RaiseAndSetIfChanged(ref _title, value); } }
public string Length { get => _length; protected set { this.RaiseAndSetIfChanged(ref _length, value); } }
public string Authors { get => _authors; protected set { this.RaiseAndSetIfChanged(ref _authors, value); } }
public string Narrators { get => _narrators; protected set { this.RaiseAndSetIfChanged(ref _narrators, value); } }
public string Category { get => _category; protected set { this.RaiseAndSetIfChanged(ref _category, value); } }
public string Misc { get => _misc; protected set { this.RaiseAndSetIfChanged(ref _misc, value); } }
public string Description { get => _description; protected set { this.RaiseAndSetIfChanged(ref _description, value); } }
public string ProductRating { get => _productRating; protected set { this.RaiseAndSetIfChanged(ref _productRating, value); } }
public string MyRating { get => _myRating; protected set { this.RaiseAndSetIfChanged(ref _myRating, value); } }
public string PurchaseDate { get; protected set; }
public string Series { get; protected set; }
public string Title { get; protected set; }
public string Length { get; protected set; }
public string Authors { get; protected set; }
public string Narrators { get; protected set; }
public string Category { get; protected set; }
public string Misc { get; protected set; }
public string Description { get; protected set; }
public string ProductRating { get; protected set; }
public string MyRating { get; protected set; }
protected bool? _remove = false;
public abstract bool? Remove { get; set; }
public abstract LiberateButtonStatus2 Liberate { get; }
public abstract BookTags BookTags { get; }
public abstract bool IsSeries { get; }
public abstract bool IsEpisode { get; }
public abstract bool IsBook { get; }
#endregion

View File

@ -48,6 +48,13 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
base.Remove(entry);
}
public void ReplaceList(IEnumerable<GridEntry2> newItems)
{
Items.Clear();
((List<GridEntry2>)Items).AddRange(newItems);
ResetCollection();
}
protected override void InsertItem(int index, GridEntry2 item)
{
FilterRemoved.Remove(item);
@ -120,10 +127,22 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
{
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
{
Remove(episode);
/*
* Bypass ObservationCollection's InsertItem methos so that CollectionChanged isn't
* fired. When adding many items at once, Avalonia's CollectionChanged event handler
* causes serious performance problems. And unfotrunately, Avalonia doesn't respect
* the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
* overload that would fire only once for all changed items.
*
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
*/
FilterRemoved.Add(episode);
Items.Remove(episode);
}
sEntry.Liberate.Expanded = false;
ResetCollection();
}
public void ExpandItem(SeriesEntrys2 sEntry)
@ -134,10 +153,23 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
{
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
{
InsertItem(++sindex, episode);
/*
* Bypass ObservationCollection's InsertItem methos so that CollectionChanged isn't
* fired. When adding many items at once, Avalonia's CollectionChanged event handler
* causes serious performance problems. And unfotrunately, Avalonia doesn't respect
* the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
* overload that would fire only once for all changed items.
*
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
*/
FilterRemoved.Remove(episode);
Items.Insert(++sindex, episode);
}
}
sEntry.Liberate.Expanded = true;
ResetCollection();
}
#endregion

View File

@ -8,6 +8,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class LiberateButtonStatus2 : ViewModelBase, IComparable
{
public LiberateButtonStatus2(bool isSeries)
{
IsSeries = isSeries;
}
public LiberatedStatus BookStatus { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
@ -22,11 +26,11 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
this.RaisePropertyChanged(nameof(ToolTip));
}
}
public bool IsSeries { get; init; }
private bool IsSeries { get; }
public Bitmap Image => GetLiberateIcon();
public string ToolTip => GetTooltip();
static Dictionary<string, Bitmap> images = new();
static Dictionary<string, Bitmap> iconCache = new();
/// <summary> Defines the Liberate column's sorting behavior </summary>
public int CompareTo(object obj)
@ -106,14 +110,14 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
private static Bitmap GetFromResources(string rescName)
{
if (images.ContainsKey(rescName)) return images[rescName];
if (iconCache.ContainsKey(rescName)) return iconCache[rescName];
var memoryStream = new System.IO.MemoryStream();
((System.Drawing.Bitmap)Properties.Resources.ResourceManager.GetObject(rescName)).Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
memoryStream.Position = 0;
images[rescName] = new Bitmap(memoryStream);
return images[rescName];
iconCache[rescName] = new Bitmap(memoryStream);
return iconCache[rescName];
}
}
}

View File

@ -1,7 +1,6 @@
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using LibationWinForms.GridView;
using ReactiveUI;
using System;
using System.Collections.Generic;
@ -27,7 +26,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
get => _remove;
set
{
_remove = value.HasValue ? value.Value : false;
_remove = value ?? false;
Parent?.ChildRemoveUpdate();
this.RaisePropertyChanged(nameof(Remove));
@ -45,32 +44,22 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
_pdfStatus = LibraryCommands.Pdf_Status(LibraryBook.Book);
lastStatusUpdate = DateTime.Now;
}
return new LiberateButtonStatus2 { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
return new LiberateButtonStatus2(IsSeries) { BookStatus = _bookStatus, PdfStatus = _pdfStatus };
}
}
public override BookTags BookTags => new() { Tags = string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated) };
public override bool IsSeries => false;
public override bool IsEpisode => Parent is not null;
public override bool IsBook => Parent is null;
#endregion
public LibraryBookEntry2(LibraryBook libraryBook)
{
setLibraryBook(libraryBook);
LoadCover();
}
public void UpdateLibraryBook(LibraryBook libraryBook)
{
if (AudibleProductId != libraryBook.Book.AudibleProductId)
throw new Exception("Invalid grid entry update. IDs must match");
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
setLibraryBook(libraryBook);
}
private void setLibraryBook(LibraryBook libraryBook)
{
LibraryBook = libraryBook;
LoadCover();
Title = Book.Title;
Series = Book.SeriesNames();

View File

@ -13,7 +13,21 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
public class ProductsDisplayViewModel : ViewModelBase
{
public GridEntryBindingList2 GridEntries { get; set; }
public DataGridCollectionView GridCollectionView { get; set; }
public ProductsDisplayViewModel(IEnumerable<LibraryBook> dbBooks)
{
GridEntries = new GridEntryBindingList2(CreateGridEntries(dbBooks));
GridEntries.CollapseAll();
/*
* Would be nice to use built-in groups, but Avalonia doesn't yet let you customize the row group header.
*
GridCollectionView = new DataGridCollectionView(GridEntries);
GridCollectionView.GroupDescriptions.Add(new CustonGroupDescription());
*/
}
public static IEnumerable<GridEntry2> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
{
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
@ -36,9 +50,22 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
}
GridEntries = new GridEntryBindingList2(geList.OrderByDescending(e => e.DateAdded));
GridEntries.CollapseAll();
return geList.OrderByDescending(e => e.DateAdded);
}
}
class CustonGroupDescription : DataGridGroupDescription
{
public override object GroupKeyFromItem(object item, int level, CultureInfo culture)
{
if (item is SeriesEntrys2 sEntry)
return sEntry;
else if (item is LibraryBookEntry2 lbEntry && lbEntry.Parent is SeriesEntrys2 sEntry2)
return sEntry2;
else return null;
}
public override bool KeysMatch(object groupKey, object itemKey)
{
return base.KeysMatch(groupKey, itemKey);
}
}
}

View File

@ -3,21 +3,20 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
namespace LibationWinForms.AvaloniaUI.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.
/// 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<GridEntry2>
internal class RowComparer : IComparer
{
private static readonly System.Reflection.PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
private static readonly System.Reflection.PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
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; }
@ -34,7 +33,6 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
PropertyName = propertyName;
}
public int Compare(object x, object y)
{
if (x is null && y is not null) return -1;
@ -54,71 +52,26 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
if (geB is LibraryBookEntry2 lbB && lbB.Parent is SeriesEntrys2 seB)
parentB = seB;
if (geA is SeriesEntrys2 && geB is SeriesEntrys2)
{
//Both are parents. Make sure they never compare equal.
var comparison = InternalCompare(geA, geB);
if (comparison == 0)
{
var propBackup = PropertyName;
PropertyName = nameof(GridEntry2.Series);
comparison = InternalCompare(geA, geB);
PropertyName = propBackup;
return comparison;
}
return comparison;
}
//both a and b are standalone
//both a and b are top-level grid entries
if (parentA is null && parentB is null)
return InternalCompare(geA, geB);
//a is a standalone, b is a child
//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 if (geA is SeriesEntrys2)
{
//Both are parents. Make sure they never compare equal.
var comparison = InternalCompare(geA, parentB);
if (comparison == 0)
{
var propBackup = PropertyName;
PropertyName = nameof(GridEntry2.Series);
comparison = InternalCompare(geA, parentB);
PropertyName = propBackup;
return comparison;
}
return comparison;
}
else
return InternalCompare(geA, parentB);
}
//a is a child, b is a standalone
//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 if (geB is SeriesEntrys2)
{
//Both are parents. Make sure they never compare equal.
var comparison = InternalCompare(parentA, geB);
if (comparison == 0)
{
var propBackup = PropertyName;
PropertyName = nameof(GridEntry2.Series);
comparison = InternalCompare(parentA, geB);
PropertyName = propBackup;
return comparison;
}
return comparison;
}
else
return InternalCompare(parentA, geB);
}
@ -127,17 +80,8 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
if (parentA == parentB)
return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (SortDirection is ListSortDirection.Ascending ? 1 : -1);
//a and b are children of different series. Make sure their parents never compare equal.
var comparison2 = InternalCompare(parentA, parentB);
if (comparison2 == 0)
{
var propBackup = PropertyName;
PropertyName = nameof(GridEntry2.Series);
comparison2 = InternalCompare(parentA, parentB);
PropertyName = propBackup;
return comparison2;
}
return comparison2;
//a and b are children of different series.
return Compare(parentA, parentB);
}
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
@ -149,17 +93,14 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
var val1 = x.GetMemberValue(PropertyName);
var val2 = y.GetMemberValue(PropertyName);
return x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
}
var compareResult = x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
public int CompareTo(GridEntry2 other)
{
return Compare(this, other);
}
public int Compare(GridEntry2 x, GridEntry2 y)
{
return Compare((object)x, (object)y) * (SortDirection is ListSortDirection.Ascending ? 1 : -1);
//If items compare equal, compare them by their positions in the the list.
//This is how you achieve a stable sort.
if (compareResult == 0)
return x.ListIndex.CompareTo(y.ListIndex);
else
return compareResult;
}
}
}

View File

@ -1,4 +1,5 @@
using DataLayer;
using Avalonia.Media;
using DataLayer;
using Dinah.Core;
using ReactiveUI;
using System;
@ -31,7 +32,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
get => _remove;
set
{
_remove = value.HasValue ? value.Value : false;
_remove = value ?? false;
suspendCounting = true;
@ -46,39 +47,28 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
public override LiberateButtonStatus2 Liberate { get; }
public override BookTags BookTags { get; } = new();
public override bool IsSeries => true;
public override bool IsEpisode => false;
public override bool IsBook => false;
#endregion
private SeriesEntrys2(LibraryBook parent)
public SeriesEntrys2(LibraryBook parent, IEnumerable<LibraryBook> children)
{
Liberate = new LiberateButtonStatus2 { IsSeries = true };
Liberate = new LiberateButtonStatus2(IsSeries);
SeriesIndex = -1;
LibraryBook = parent;
LoadCover();
}
public SeriesEntrys2(LibraryBook parent, IEnumerable<LibraryBook> children) : this(parent)
{
LoadCover();
Children = children
.Select(c => new LibraryBookEntry2(c) { Parent = this })
.OrderBy(c => c.SeriesIndex)
.ToList();
UpdateSeries(parent);
}
public SeriesEntrys2(LibraryBook parent, LibraryBook child) : this(parent)
{
Children = new() { new LibraryBookEntry2(child) { Parent = this } };
UpdateSeries(parent);
}
public void UpdateSeries(LibraryBook parent)
{
LibraryBook = parent;
Title = Book.Title;
Series = Book.SeriesNames();
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
Authors = Book.AuthorNames();
Narrators = Book.NarratorNames();
@ -87,10 +77,12 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
LongDescription = GetDescriptionDisplay(Book);
Description = TrimTextToWord(LongDescription, 62);
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
}
#region Data Sorting
/// <summary>Create getters for all member object values by name</summary>

View File

@ -40,7 +40,7 @@ namespace LibationWinForms.AvaloniaUI.Views
processBookQueue1.AddDownloadDecrypt(
productsDisplay
.GetVisible()
.GetVisibleBookEntries()
.UnLiberated()
);
}
@ -56,7 +56,7 @@ namespace LibationWinForms.AvaloniaUI.Views
if (result != System.Windows.Forms.DialogResult.OK)
return;
var visibleLibraryBooks = productsDisplay.GetVisible();
var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
@ -78,7 +78,7 @@ namespace LibationWinForms.AvaloniaUI.Views
if (result != System.Windows.Forms.DialogResult.OK)
return;
var visibleLibraryBooks = productsDisplay.GetVisible();
var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
@ -95,7 +95,7 @@ namespace LibationWinForms.AvaloniaUI.Views
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var visibleLibraryBooks = productsDisplay.GetVisible();
var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
@ -121,13 +121,13 @@ namespace LibationWinForms.AvaloniaUI.Views
});
//Not used for anything?
var notLiberatedCount = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
var notLiberatedCount = productsDisplay.GetVisibleBookEntries().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
await Task.Run(setLiberatedVisibleMenuItem);
}
void setLiberatedVisibleMenuItem()
{
var notLiberated = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
var notLiberated = productsDisplay.GetVisibleBookEntries().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
Dispatcher.UIThread.Post(() =>
{

View File

@ -5,6 +5,7 @@
xmlns:vm="clr-namespace:LibationWinForms.AvaloniaUI.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:LibationWinForms.AvaloniaUI.Views"
xmlns:prgid="clr-namespace:LibationWinForms.AvaloniaUI.Views.ProductsGrid"
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="2000" d:DesignHeight="700"
x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow" Title="MainWindow">
@ -145,7 +146,7 @@
</SplitView.Pane>
<!-- Product Display Grid -->
<views:ProductsDisplay2
<prgid:ProductsDisplay2
InitialLoaded="productsDisplay_Initialized"
LiberateClicked="ProductsDisplay_LiberateClicked"
RemovableCountChanged="productsDisplay_RemovableCountChanged"

View File

@ -12,6 +12,7 @@ using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Threading;
using LibationWinForms.AvaloniaUI.Views.ProductsGrid;
namespace LibationWinForms.AvaloniaUI.Views
{

View File

@ -1,523 +0,0 @@
using ApplicationServices;
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using DataLayer;
using Dinah.Core.DataBinding;
using FileLiberator;
using LibationFileManager;
using LibationWinForms.AvaloniaUI.ViewModels;
using ReactiveUI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class ProductsDisplay2 : UserControl
{
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event EventHandler<LibraryBook> LiberateClicked;
public event EventHandler InitialLoaded;
private ProductsDisplayViewModel _viewModel;
private GridEntryBindingList2 bindingList => _viewModel.GridEntries;
private IEnumerable<LibraryBookEntry2> GetAllBookEntries()
=> bindingList.AllItems().BookEntries();
internal List<LibraryBook> GetVisible()
=> bindingList
.BookEntries()
.Select(lbe => lbe.LibraryBook)
.ToList();
DataGridColumn removeGVColumn;
DataGridColumn liberateGVColumn;
DataGridColumn coverGVColumn;
DataGridColumn titleGVColumn;
DataGridColumn authorsGVColumn;
DataGridColumn narratorsGVColumn;
DataGridColumn lengthGVColumn;
DataGridColumn seriesGVColumn;
DataGridColumn descriptionGVColumn;
DataGridColumn categoryGVColumn;
DataGridColumn productRatingGVColumn;
DataGridColumn purchaseDateGVColumn;
DataGridColumn myRatingGVColumn;
DataGridColumn miscGVColumn;
DataGridColumn tagAndDetailsGVColumn;
#region Init
public ProductsDisplay2()
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
var book = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(new List<LibraryBook> { book });
return;
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
productsGrid = this.FindControl<DataGrid>(nameof(productsGrid));
productsGrid.Sorting += ProductsGrid_Sorting;
productsGrid.CanUserSortColumns = true;
productsGrid.LoadingRow += ProductsGrid_LoadingRow;
removeGVColumn = productsGrid.Columns[0];
liberateGVColumn = productsGrid.Columns[1];
coverGVColumn = productsGrid.Columns[2];
titleGVColumn = productsGrid.Columns[3];
authorsGVColumn = productsGrid.Columns[4];
narratorsGVColumn = productsGrid.Columns[5];
lengthGVColumn = productsGrid.Columns[6];
seriesGVColumn = productsGrid.Columns[7];
descriptionGVColumn = productsGrid.Columns[8];
categoryGVColumn = productsGrid.Columns[9];
productRatingGVColumn = productsGrid.Columns[10];
purchaseDateGVColumn = productsGrid.Columns[11];
myRatingGVColumn = productsGrid.Columns[12];
miscGVColumn = productsGrid.Columns[13];
tagAndDetailsGVColumn = productsGrid.Columns[14];
RegisterCustomColumnComparers();
}
#endregion
#region Apply Background Brush Style to Series Books Rows
private static object tagObj = new();
private void ProductsGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
if (e.Row.Tag == tagObj)
return;
e.Row.Tag = tagObj;
static IBrush GetRowColor(DataGridRow row)
=> row.DataContext is GridEntry2 gEntry
&& gEntry is LibraryBookEntry2 lbEntry
&& lbEntry.Parent is not null
? App.SeriesEntryGridBackgroundBrush
: null;
e.Row.Background = GetRowColor(e.Row);
e.Row.DataContextChanged += (sender, e) =>
{
var row = sender as DataGridRow;
row.Background = GetRowColor(row);
};
}
#endregion
#region Filter
public void Filter(string searchString)
{
int visibleCount = bindingList.Count;
if (string.IsNullOrEmpty(searchString))
bindingList.RemoveFilter();
else
bindingList.Filter = searchString;
if (visibleCount != bindingList.Count)
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
//Re-sort after filtering
ReSort();
}
#endregion
#region Sorting
private void RegisterCustomColumnComparers()
{
removeGVColumn.CustomSortComparer = new RowComparer(removeGVColumn);
liberateGVColumn.CustomSortComparer = new RowComparer(liberateGVColumn);
titleGVColumn.CustomSortComparer = new RowComparer(titleGVColumn);
authorsGVColumn.CustomSortComparer = new RowComparer(authorsGVColumn);
narratorsGVColumn.CustomSortComparer = new RowComparer(narratorsGVColumn);
lengthGVColumn.CustomSortComparer = new RowComparer(lengthGVColumn);
seriesGVColumn.CustomSortComparer = new RowComparer(seriesGVColumn);
descriptionGVColumn.CustomSortComparer = new RowComparer(descriptionGVColumn);
categoryGVColumn.CustomSortComparer = new RowComparer(categoryGVColumn);
productRatingGVColumn.CustomSortComparer = new RowComparer(productRatingGVColumn);
purchaseDateGVColumn.CustomSortComparer = new RowComparer(purchaseDateGVColumn);
myRatingGVColumn.CustomSortComparer = new RowComparer(myRatingGVColumn);
miscGVColumn.CustomSortComparer = new RowComparer(miscGVColumn);
tagAndDetailsGVColumn.CustomSortComparer = new RowComparer(tagAndDetailsGVColumn);
}
private void ReSort()
{
if (CurrentSortColumn is null)
{
bindingList.InternalList.Sort(new RowComparer(ListSortDirection.Descending, nameof(GridEntry2.DateAdded)));
bindingList.ResetCollection();
}
else
CurrentSortColumn.Sort(((RowComparer)CurrentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
}
private DataGridColumn CurrentSortColumn;
private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
{
var comparer = e.Column.CustomSortComparer as RowComparer;
//Force the comparer to get the current sort order. We can't
//retrieve it from inside this event handler because Avalonia
//doesn't set the property until after this event.
comparer.SortDirection = null;
CurrentSortColumn = e.Column;
}
#endregion
#region Button controls
public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is SeriesEntrys2 sEntry)
{
if (sEntry.Liberate.Expanded)
bindingList.CollapseItem(sEntry);
else
{
bindingList.ExpandItem(sEntry);
ReSort();
}
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
else if (button.DataContext is LibraryBookEntry2 lbEntry)
{
LiberateClicked?.Invoke(this, lbEntry.LibraryBook);
}
}
private GridView.ImageDisplay imageDisplay;
public async void Cover_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
if (sender is not Image tblock || tblock.DataContext is not GridEntry2 gEntry)
return;
var picDefinition = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
(_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(gEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
var windowTitle = $"{gEntry.Title} - Cover";
if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
{
imageDisplay = new GridView.ImageDisplay();
imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
imageDisplay.Show(null);
}
imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(gEntry.LibraryBook);
imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(gEntry.LibraryBook, ".jpg"));
imageDisplay.Text = windowTitle;
imageDisplay.CoverPicture = initialImageBts;
imageDisplay.CoverPicture = await picDlTask;
}
public void Description_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
if (sender is TextBlock tblock && tblock.DataContext is GridEntry2 gEntry)
{
var pt = tblock.Parent.PointToScreen(tblock.Parent.Bounds.TopRight);
var displayWindow = new GridView.DescriptionDisplay
{
SpawnLocation = new System.Drawing.Point(pt.X, pt.Y),
DescriptionText = gEntry.LongDescription,
BorderThickness = 2,
};
void CloseWindow(object o, DataGridRowEventArgs e)
{
displayWindow.Close();
}
productsGrid.LoadingRow += CloseWindow;
displayWindow.FormClosed += (_, _) =>
{
productsGrid.LoadingRow -= CloseWindow;
};
displayWindow.Show();
}
}
public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is LibraryBookEntry2 lbEntry)
{
var bookDetailsForm = new Dialogs.BookDetailsDialog(lbEntry.LibraryBook);
if (bookDetailsForm.ShowDialog() == System.Windows.Forms.DialogResult.OK)
lbEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
}
}
#endregion
#region Scan and Remove Books
public void CloseRemoveBooksColumn()
=> removeGVColumn.IsVisible = false;
public async Task RemoveCheckedBooksAsync()
{
var selectedBooks = GetAllBookEntries().Where(lbe => lbe.Remove == true).ToList();
if (selectedBooks.Count == 0)
return;
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = MessageBoxLib.ShowConfirmationDialog(
libraryBooks,
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
"Remove books from Libation?");
if (result != System.Windows.Forms.DialogResult.Yes)
return;
RemoveBooks(selectedBooks);
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
}
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
{
RemovableCountChanged?.Invoke(this, 0);
removeGVColumn.IsVisible = true;
try
{
if (accounts is null || accounts.Length == 0)
return;
var allBooks = GetAllBookEntries();
foreach (var b in allBooks)
b.Remove = false;
var lib = allBooks
.Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated());
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts);
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
foreach (var r in removable)
r.Remove = true;
RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
null,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",
ex);
}
}
#endregion
#region UI display functions
public void Display()
{
try
{
// don't return early if lib size == 0. this will not update correctly if all books are removed
var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
if (productsGrid.DataContext is null)
{
productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(dbBooks);
InitialLoaded?.Invoke(this, EventArgs.Empty);
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
else
UpdateGrid(dbBooks);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay2));
}
}
private void UpdateGrid(List<LibraryBook> dbBooks)
{
#region Add new or update existing grid entries
//Remove filter prior to adding/updating boooks
string existingFilter = bindingList.Filter;
Filter(null);
bindingList.SuspendFilteringOnUpdate = true;
//Add absent entries to grid, or update existing entry
var allEntries = bindingList.AllItems().BookEntries();
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
var parentedEpisodes = dbBooks.ParentedEpisodes();
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
{
var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
if (libraryBook.Book.IsProduct())
AddOrUpdateBook(libraryBook, existingEntry);
else if (parentedEpisodes.Any(lb => lb == libraryBook))
//Only try to add or update is this LibraryBook is a know child of a parent
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
}
bindingList.SuspendFilteringOnUpdate = false;
//Re-apply filter after adding new/updating existing books to capture any changes
Filter(existingFilter);
#endregion
// remove deleted from grid.
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
var removedBooks =
bindingList
.AllItems()
.BookEntries()
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
RemoveBooks(removedBooks);
}
private void RemoveBooks(IEnumerable<LibraryBookEntry2> removedBooks)
{
//Remove books in series from their parents' Children list
foreach (var removed in removedBooks.Where(b => b.Parent is not null))
{
removed.Parent.Children.Remove(removed);
//In Avalonia, if you fire PropertyChanged with an empty or invalid property name, nothing is updated.
//So we must notify for specific properties that we believed changed.
removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.Length));
removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.PurchaseDate));
}
//Remove series that have no children
var removedSeries =
bindingList
.AllItems()
.EmptySeries();
foreach (var removed in removedBooks.Cast<GridEntry2>().Concat(removedSeries))
//no need to re-filter for removed books
bindingList.Remove(removed);
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
private void AddOrUpdateBook(LibraryBook book, LibraryBookEntry2 existingBookEntry)
{
if (existingBookEntry is null)
// Add the new product to top
bindingList.Insert(0, new LibraryBookEntry2(book));
else
// update existing
existingBookEntry.UpdateLibraryBook(book);
}
private void AddOrUpdateEpisode(LibraryBook episodeBook, LibraryBookEntry2 existingEpisodeEntry, List<SeriesEntrys2> seriesEntries, IEnumerable<LibraryBook> dbBooks)
{
if (existingEpisodeEntry is null)
{
LibraryBookEntry2 episodeEntry;
var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
if (seriesEntry is null)
{
//Series doesn't exist yet, so create and add it
var seriesBook = dbBooks.FindSeriesParent(episodeBook);
if (seriesBook is null)
{
//This is only possible if the user's db has some malformed
//entries from earlier Libation releases that could not be
//automatically fixed. Log, but don't throw.
Serilog.Log.Logger.Error("Episode={0}, Episode Series: {1}", episodeBook, episodeBook.Book.SeriesNames());
return;
}
seriesEntry = new SeriesEntrys2(seriesBook, episodeBook);
seriesEntries.Add(seriesEntry);
episodeEntry = seriesEntry.Children[0];
seriesEntry.Liberate.Expanded = true;
bindingList.Insert(0, seriesEntry);
}
else
{
//Series exists. Create and add episode child then update the SeriesEntry
episodeEntry = new(episodeBook) { Parent = seriesEntry };
seriesEntry.Children.Add(episodeEntry);
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
seriesEntry.UpdateSeries(seriesBook);
}
//Add episode to the grid beneath the parent
int seriesIndex = bindingList.IndexOf(seriesEntry);
bindingList.Insert(seriesIndex + 1, episodeEntry);
if (seriesEntry.Liberate.Expanded)
bindingList.ExpandItem(seriesEntry);
else
bindingList.CollapseItem(seriesEntry);
seriesEntry.RaisePropertyChanged(nameof(SeriesEntrys2.Length));
seriesEntry.RaisePropertyChanged(nameof(SeriesEntrys2.PurchaseDate));
}
else
existingEpisodeEntry.UpdateLibraryBook(episodeBook);
}
#endregion
#region Column Customizations
#endregion
}
}

View File

@ -0,0 +1,109 @@
using Avalonia;
using Avalonia.Controls;
using FileLiberator;
using LibationFileManager;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private GridView.ImageDisplay imageDisplay;
private void Configure_Buttons() { }
public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is SeriesEntrys2 sEntry)
{
if (sEntry.Liberate.Expanded)
{
bindingList.CollapseItem(sEntry);
}
else
{
bindingList.ExpandItem(sEntry);
}
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
//Expanding and collapsing reset the list, which will cause focus to shift
//to the topright cell. Reset focus onto the clicked button's cell.
((sender as Control).Parent.Parent as DataGridCell)?.Focus();
}
else if (button.DataContext is LibraryBookEntry2 lbEntry)
{
LiberateClicked?.Invoke(this, lbEntry.LibraryBook);
}
}
public async void Cover_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
if (sender is not Image tblock || tblock.DataContext is not GridEntry2 gEntry)
return;
var picDefinition = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
(_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(gEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
var windowTitle = $"{gEntry.Title} - Cover";
if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
{
imageDisplay = new GridView.ImageDisplay();
imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
imageDisplay.Show(null);
}
imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(gEntry.LibraryBook);
imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(gEntry.LibraryBook, ".jpg"));
imageDisplay.Text = windowTitle;
imageDisplay.CoverPicture = initialImageBts;
imageDisplay.CoverPicture = await picDlTask;
}
public void Description_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
if (sender is TextBlock tblock && tblock.DataContext is GridEntry2 gEntry)
{
var pt = tblock.Parent.PointToScreen(tblock.Parent.Bounds.TopRight);
var displayWindow = new GridView.DescriptionDisplay
{
SpawnLocation = new System.Drawing.Point(pt.X, pt.Y),
DescriptionText = gEntry.LongDescription,
BorderThickness = 2,
};
void CloseWindow(object o, DataGridRowEventArgs e)
{
displayWindow.Close();
}
productsGrid.LoadingRow += CloseWindow;
displayWindow.FormClosed += (_, _) =>
{
productsGrid.LoadingRow -= CloseWindow;
};
displayWindow.Show();
}
}
public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is LibraryBookEntry2 lbEntry)
{
var bookDetailsForm = new Dialogs.BookDetailsDialog(lbEntry.LibraryBook);
if (bookDetailsForm.ShowDialog() == System.Windows.Forms.DialogResult.OK)
lbEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
}
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private void Configure_ColumnCustomization() { }
}
}

View File

@ -0,0 +1,62 @@
using ApplicationServices;
using Avalonia.Controls;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Collections;
using System.Linq;
using System.Reflection;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private void Configure_Display() { }
public void Display()
{
try
{
var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
if (_viewModel is null)
{
_viewModel = new ProductsDisplayViewModel(dbBooks);
InitialLoaded?.Invoke(this, EventArgs.Empty);
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
//Avalonia displays items in the DataConncetion from an internal copy of
//the bound list, not the actual bound list. so we need to reflect to get
//the current display order and set the GridEntry.ListIndex correctly.
var DataConnection_PI = typeof(DataGrid).GetProperty("DataConnection", BindingFlags.NonPublic | BindingFlags.Instance);
var DataSource_PI = DataConnection_PI.PropertyType.GetProperty("DataSource", BindingFlags.Public | BindingFlags.Instance);
bindingList.CollectionChanged += (s, e) =>
{
var displayListGE = ((IEnumerable)DataSource_PI.GetValue(DataConnection_PI.GetValue(productsGrid))).Cast<GridEntry2>();
int index = 0;
foreach (var di in displayListGE)
{
di.ListIndex = index++;
}
};
//Assign the viewmodel after we subscribe to CollectionChanged
//to ensure that out handler executes first.
productsGrid.DataContext = _viewModel;
}
else
{
string existingFilter = _viewModel?.GridEntries?.Filter;
bindingList.ReplaceList(ProductsDisplayViewModel.CreateGridEntries(dbBooks));
bindingList.Filter = existingFilter;
ReSort();
}
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay2));
}
}
}
}

View File

@ -0,0 +1,27 @@
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private void Configure_Filtering() { }
public void Filter(string searchString)
{
int visibleCount = bindingList.Count;
if (string.IsNullOrEmpty(searchString))
bindingList.RemoveFilter();
else
bindingList.Filter = searchString;
if (visibleCount != bindingList.Count)
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
//Re-sort after filtering
ReSort();
}
}
}

View File

@ -0,0 +1,106 @@
using ApplicationServices;
using AudibleUtilities;
using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private void Configure_ScanAndRemove() { }
public void CloseRemoveBooksColumn()
=> removeGVColumn.IsVisible = false;
public async Task RemoveCheckedBooksAsync()
{
var selectedBooks = GetAllBookEntries().Where(lbe => lbe.Remove == true).ToList();
if (selectedBooks.Count == 0)
return;
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = MessageBoxLib.ShowConfirmationDialog(
libraryBooks,
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
"Remove books from Libation?");
if (result != System.Windows.Forms.DialogResult.Yes)
return;
RemoveBooks(selectedBooks);
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
}
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
{
RemovableCountChanged?.Invoke(this, 0);
removeGVColumn.IsVisible = true;
try
{
if (accounts is null || accounts.Length == 0)
return;
var allBooks = GetAllBookEntries();
foreach (var b in allBooks)
b.Remove = false;
var lib = allBooks
.Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated());
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts);
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
foreach (var r in removable)
r.Remove = true;
RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
null,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",
ex);
}
}
private void RemoveBooks(IEnumerable<LibraryBookEntry2> removedBooks)
{
//Remove books in series from their parents' Children list
foreach (var removed in removedBooks.Where(b => b.Parent is not null))
{
removed.Parent.Children.Remove(removed);
//In Avalonia, if you fire PropertyChanged with an empty or invalid property name, nothing is updated.
//So we must notify for specific properties that we believed changed.
removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.Length));
removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.PurchaseDate));
}
//Remove series that have no children
var removedSeries =
bindingList
.AllItems()
.EmptySeries();
foreach (var removed in removedBooks.Cast<GridEntry2>().Concat(removedSeries))
//no need to re-filter for removed books
bindingList.Remove(removed);
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
}
}

View File

@ -0,0 +1,65 @@
using Avalonia.Controls;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private void Configure_Sorting() { }
private void RegisterCustomColumnComparers()
{
removeGVColumn.CustomSortComparer = new RowComparer(removeGVColumn);
liberateGVColumn.CustomSortComparer = new RowComparer(liberateGVColumn);
titleGVColumn.CustomSortComparer = new RowComparer(titleGVColumn);
authorsGVColumn.CustomSortComparer = new RowComparer(authorsGVColumn);
narratorsGVColumn.CustomSortComparer = new RowComparer(narratorsGVColumn);
lengthGVColumn.CustomSortComparer = new RowComparer(lengthGVColumn);
seriesGVColumn.CustomSortComparer = new RowComparer(seriesGVColumn);
descriptionGVColumn.CustomSortComparer = new RowComparer(descriptionGVColumn);
categoryGVColumn.CustomSortComparer = new RowComparer(categoryGVColumn);
productRatingGVColumn.CustomSortComparer = new RowComparer(productRatingGVColumn);
purchaseDateGVColumn.CustomSortComparer = new RowComparer(purchaseDateGVColumn);
myRatingGVColumn.CustomSortComparer = new RowComparer(myRatingGVColumn);
miscGVColumn.CustomSortComparer = new RowComparer(miscGVColumn);
tagAndDetailsGVColumn.CustomSortComparer = new RowComparer(tagAndDetailsGVColumn);
}
private void ReSort()
{
if (CurrentSortColumn is null)
{
bindingList.InternalList.Sort((i1, i2) => i2.DateAdded.CompareTo(i1.DateAdded));
bindingList.ResetCollection();
}
else
{
CurrentSortColumn.Sort(((RowComparer)CurrentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
}
}
private DataGridColumn CurrentSortColumn;
private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
{
//Force the comparer to get the current sort order. We can't
//retrieve it from inside this event handler because Avalonia
//doesn't set the property until after this event.
var comparer = e.Column.CustomSortComparer as RowComparer;
comparer.SortDirection = null;
CurrentSortColumn = e.Column;
}
}
}

View File

@ -5,10 +5,11 @@
xmlns:views="clr-namespace:LibationWinForms.AvaloniaUI.Views"
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="1560" d:DesignHeight="400"
x:Class="LibationWinForms.AvaloniaUI.Views.ProductsDisplay2">
x:Class="LibationWinForms.AvaloniaUI.Views.ProductsGrid.ProductsDisplay2">
<Grid>
<DataGrid Name="productsGrid" AutoGenerateColumns="False" Items="{Binding GridEntries}" >
<DataGrid.Columns>
<controls:DataGridCheckBoxColumnExt IsVisible="False" Header="Remove" IsThreeState="True" IsReadOnly="False" CanUserSort="True" Binding="{Binding Remove, Mode=TwoWay}" Width="70" SortMemberPath="Remove"/>
@ -147,7 +148,7 @@
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button IsVisible="{Binding !Liberate.IsSeries}" Width="100" Height="80" Click="OnTagsButtonClick" ToolTip.Tip="Click to edit tags" >
<Button IsVisible="{Binding !IsSeries}" Width="100" Height="80" Click="OnTagsButtonClick" ToolTip.Tip="Click to edit tags" >
<Panel>
<Image IsVisible="{Binding !BookTags.HasTags}" Stretch="None" Source="/AvaloniaUI/Assets/edit_25x25.png" />
<TextBlock IsVisible="{Binding BookTags.HasTags}" FontSize="12" TextWrapping="WrapWithOverflow" Text="{Binding BookTags.Tags}"/>
@ -160,5 +161,6 @@
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View File

@ -0,0 +1,121 @@
using ApplicationServices;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2 : UserControl
{
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event EventHandler<LibraryBook> LiberateClicked;
public event EventHandler InitialLoaded;
public List<LibraryBook> GetVisibleBookEntries()
=> bindingList
.BookEntries()
.Select(lbe => lbe.LibraryBook)
.ToList();
private IEnumerable<LibraryBookEntry2> GetAllBookEntries()
=> bindingList
.AllItems()
.BookEntries();
private ProductsDisplayViewModel _viewModel;
private GridEntryBindingList2 bindingList => _viewModel.GridEntries;
DataGridColumn removeGVColumn;
DataGridColumn liberateGVColumn;
DataGridColumn coverGVColumn;
DataGridColumn titleGVColumn;
DataGridColumn authorsGVColumn;
DataGridColumn narratorsGVColumn;
DataGridColumn lengthGVColumn;
DataGridColumn seriesGVColumn;
DataGridColumn descriptionGVColumn;
DataGridColumn categoryGVColumn;
DataGridColumn productRatingGVColumn;
DataGridColumn purchaseDateGVColumn;
DataGridColumn myRatingGVColumn;
DataGridColumn miscGVColumn;
DataGridColumn tagAndDetailsGVColumn;
public ProductsDisplay2()
{
InitializeComponent();
Configure_Buttons();
Configure_ColumnCustomization();
Configure_Display();
Configure_Filtering();
Configure_ScanAndRemove();
Configure_Sorting();
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
var book = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(new List<LibraryBook> { book });
return;
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
productsGrid = this.FindControl<DataGrid>(nameof(productsGrid));
productsGrid.Sorting += ProductsGrid_Sorting;
productsGrid.CanUserSortColumns = true;
productsGrid.LoadingRow += ProductsGrid_LoadingRow;
removeGVColumn = productsGrid.Columns[0];
liberateGVColumn = productsGrid.Columns[1];
coverGVColumn = productsGrid.Columns[2];
titleGVColumn = productsGrid.Columns[3];
authorsGVColumn = productsGrid.Columns[4];
narratorsGVColumn = productsGrid.Columns[5];
lengthGVColumn = productsGrid.Columns[6];
seriesGVColumn = productsGrid.Columns[7];
descriptionGVColumn = productsGrid.Columns[8];
categoryGVColumn = productsGrid.Columns[9];
productRatingGVColumn = productsGrid.Columns[10];
purchaseDateGVColumn = productsGrid.Columns[11];
myRatingGVColumn = productsGrid.Columns[12];
miscGVColumn = productsGrid.Columns[13];
tagAndDetailsGVColumn = productsGrid.Columns[14];
RegisterCustomColumnComparers();
}
private static object tagObj = new();
private void ProductsGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
if (e.Row.Tag == tagObj)
return;
e.Row.Tag = tagObj;
static IBrush GetRowColor(DataGridRow row)
=> row.DataContext is GridEntry2 gEntry
&& gEntry is LibraryBookEntry2 lbEntry
&& lbEntry.Parent is not null
? App.SeriesEntryGridBackgroundBrush
: null;
e.Row.Background = GetRowColor(e.Row);
e.Row.DataContextChanged += (sender, e) =>
{
var row = sender as DataGridRow;
row.Background = GetRowColor(row);
};
}
}
}