Fix sorting and refactor
This commit is contained in:
parent
e33fd6ea1b
commit
3a61c32881
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(() =>
|
||||
{
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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() { }
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 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>
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user