Performance improvements and better mvvp pattern following
This commit is contained in:
parent
c2a2e51bde
commit
8cd6219bd9
BIN
Source/LibationWinForms/AvaloniaUI/Assets/libation.ico
Normal file
BIN
Source/LibationWinForms/AvaloniaUI/Assets/libation.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@ -9,16 +9,19 @@ using System.Linq;
|
|||||||
namespace LibationWinForms.AvaloniaUI.ViewModels
|
namespace LibationWinForms.AvaloniaUI.ViewModels
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Allows filtering and sorting of the underlying BindingList<GridEntry>
|
* Allows filtering of the underlying ObservableCollection<GridEntry>
|
||||||
* by implementing IBindingListView and using SearchEngineCommands
|
|
||||||
*
|
*
|
||||||
* When filtering is applied, the filtered-out items are removed
|
* When filtering is applied, the filtered-out items are removed
|
||||||
* from the base list and added to the private FilterRemoved list.
|
* from the base list and added to the private FilterRemoved list.
|
||||||
* When filtering is removed, items in the FilterRemoved list are
|
* When filtering is removed, items in the FilterRemoved list are
|
||||||
* added back to the base list.
|
* added back to the base list.
|
||||||
*
|
*
|
||||||
* Remove is overridden to ensure that removed items are removed from
|
* Items are added and removed to/from the ObservableCollection's
|
||||||
* the base list (visible items) as well as the FilterRemoved list.
|
* internal list instead of the ObservableCollection itself to
|
||||||
|
* avoid ObservableCollection firing CollectionChanged for every
|
||||||
|
* item. Editing the list this way improve's display performance,
|
||||||
|
* but requires ResetCollection() to be called after all changes
|
||||||
|
* have been made.
|
||||||
*/
|
*/
|
||||||
public class GridEntryBindingList2 : ObservableCollection<GridEntry2>
|
public class GridEntryBindingList2 : ObservableCollection<GridEntry2>
|
||||||
{
|
{
|
||||||
@ -42,31 +45,19 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
|
|||||||
|
|
||||||
#region Items Management
|
#region Items Management
|
||||||
|
|
||||||
public new void Remove(GridEntry2 entry)
|
|
||||||
{
|
|
||||||
FilterRemoved.Add(entry);
|
|
||||||
base.Remove(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ReplaceList(IEnumerable<GridEntry2> newItems)
|
public void ReplaceList(IEnumerable<GridEntry2> newItems)
|
||||||
{
|
{
|
||||||
Items.Clear();
|
Items.Clear();
|
||||||
((List<GridEntry2>)Items).AddRange(newItems);
|
((List<GridEntry2>)Items).AddRange(newItems);
|
||||||
ResetCollection();
|
ResetCollection();
|
||||||
}
|
}
|
||||||
|
public void ResetCollection()
|
||||||
protected override void InsertItem(int index, GridEntry2 item)
|
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||||
{
|
|
||||||
FilterRemoved.Remove(item);
|
|
||||||
base.InsertItem(index, item);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Filtering
|
#region Filtering
|
||||||
|
|
||||||
public void ResetCollection()
|
|
||||||
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
|
||||||
|
|
||||||
private void ApplyFilter(string filterString)
|
private void ApplyFilter(string filterString)
|
||||||
{
|
{
|
||||||
@ -85,8 +76,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
|
|||||||
|
|
||||||
foreach (var item in filteredOut)
|
foreach (var item in filteredOut)
|
||||||
{
|
{
|
||||||
Remove(item);
|
FilterRemoved.Add(item);
|
||||||
|
Items.Remove(item);
|
||||||
}
|
}
|
||||||
|
ResetCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveFilter()
|
public void RemoveFilter()
|
||||||
@ -99,12 +92,15 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
|
|||||||
{
|
{
|
||||||
if (item is SeriesEntrys2 || item is LibraryBookEntry2 lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded))
|
if (item is SeriesEntrys2 || item is LibraryBookEntry2 lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded))
|
||||||
{
|
{
|
||||||
InsertItem(visibleCount++, item);
|
|
||||||
|
FilterRemoved.Remove(item);
|
||||||
|
Items.Insert(visibleCount++, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterString = null;
|
FilterString = null;
|
||||||
SearchResults = null;
|
SearchResults = null;
|
||||||
|
ResetCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -128,10 +124,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
|
|||||||
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
|
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Bypass ObservationCollection's InsertItem methos so that CollectionChanged isn't
|
* Bypass ObservationCollection's InsertItem method so that CollectionChanged isn't
|
||||||
* fired. When adding many items at once, Avalonia's CollectionChanged event handler
|
* fired. When adding or removing many items at once, Avalonia's CollectionChanged
|
||||||
* causes serious performance problems. And unfotrunately, Avalonia doesn't respect
|
* event handler causes serious performance problems. And unfotrunately, Avalonia
|
||||||
* the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
|
* doesn't respect the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
|
||||||
* overload that would fire only once for all changed items.
|
* 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.
|
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
|
||||||
@ -154,10 +150,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
|
|||||||
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Bypass ObservationCollection's InsertItem methos so that CollectionChanged isn't
|
* Bypass ObservationCollection's InsertItem method so that CollectionChanged isn't
|
||||||
* fired. When adding many items at once, Avalonia's CollectionChanged event handler
|
* fired. When adding or removing many items at once, Avalonia's CollectionChanged
|
||||||
* causes serious performance problems. And unfotrunately, Avalonia doesn't respect
|
* event handler causes serious performance problems. And unfotrunately, Avalonia
|
||||||
* the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
|
* doesn't respect the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
|
||||||
* overload that would fire only once for all changed items.
|
* 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.
|
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
|
||||||
|
|||||||
@ -22,6 +22,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
|
|||||||
|
|
||||||
/// <summary> The Process Queue's viewmodel </summary>
|
/// <summary> The Process Queue's viewmodel </summary>
|
||||||
public ProcessQueueViewModel ProcessQueueViewModel { get; } = new ProcessQueueViewModel();
|
public ProcessQueueViewModel ProcessQueueViewModel { get; } = new ProcessQueueViewModel();
|
||||||
|
public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
|
||||||
|
|
||||||
|
|
||||||
/// <summary> Library filterting query </summary>
|
/// <summary> Library filterting query </summary>
|
||||||
|
|||||||
@ -1,31 +1,128 @@
|
|||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ReactiveUI;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Collections;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ApplicationServices;
|
||||||
|
using AudibleUtilities;
|
||||||
|
|
||||||
namespace LibationWinForms.AvaloniaUI.ViewModels
|
namespace LibationWinForms.AvaloniaUI.ViewModels
|
||||||
{
|
{
|
||||||
public class ProductsDisplayViewModel : ViewModelBase
|
public class ProductsDisplayViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public GridEntryBindingList2 GridEntries { get; set; }
|
|
||||||
public DataGridCollectionView GridCollectionView { get; set; }
|
/// <summary>Number of visible rows has changed</summary>
|
||||||
public ProductsDisplayViewModel(IEnumerable<LibraryBook> dbBooks)
|
public event EventHandler<int> VisibleCountChanged;
|
||||||
|
public event EventHandler<int> RemovableCountChanged;
|
||||||
|
public event EventHandler InitialLoaded;
|
||||||
|
|
||||||
|
private DataGridColumn _currentSortColumn;
|
||||||
|
|
||||||
|
private GridEntryBindingList2 _gridEntries;
|
||||||
|
private bool _removeColumnVisivle;
|
||||||
|
public GridEntryBindingList2 GridEntries { get => _gridEntries; private set => this.RaiseAndSetIfChanged(ref _gridEntries, value); }
|
||||||
|
public bool RemoveColumnVisivle { get => _removeColumnVisivle; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisivle, value); }
|
||||||
|
|
||||||
|
|
||||||
|
public List<LibraryBook> GetVisibleBookEntries()
|
||||||
|
=> GridEntries
|
||||||
|
.BookEntries()
|
||||||
|
.Select(lbe => lbe.LibraryBook)
|
||||||
|
.ToList();
|
||||||
|
public IEnumerable<LibraryBookEntry2> GetAllBookEntries()
|
||||||
|
=> GridEntries
|
||||||
|
.AllItems()
|
||||||
|
.BookEntries();
|
||||||
|
|
||||||
|
public ProductsDisplayViewModel()
|
||||||
|
{
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
{
|
||||||
|
using var context = DbContexts.GetContext();
|
||||||
|
var book = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
|
||||||
|
_gridEntries = new GridEntryBindingList2(CreateGridEntries(new List<LibraryBook> { book }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitialDisplay(List<LibraryBook> dbBooks, Views.ProductsGrid.ProductsDisplay2 productsGrid)
|
||||||
|
{
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
GridEntries = new GridEntryBindingList2(CreateGridEntries(dbBooks));
|
GridEntries = new GridEntryBindingList2(CreateGridEntries(dbBooks));
|
||||||
GridEntries.CollapseAll();
|
GridEntries.CollapseAll();
|
||||||
|
|
||||||
/*
|
int bookEntryCount = GridEntries.BookEntries().Count();
|
||||||
* Would be nice to use built-in groups, but Avalonia doesn't yet let you customize the row group header.
|
|
||||||
*
|
InitialLoaded?.Invoke(this, EventArgs.Empty);
|
||||||
GridCollectionView = new DataGridCollectionView(GridEntries);
|
VisibleCountChanged?.Invoke(this, bookEntryCount);
|
||||||
GridCollectionView.GroupDescriptions.Add(new CustonGroupDescription());
|
|
||||||
*/
|
//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 each 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);
|
||||||
|
|
||||||
|
GridEntries.CollectionChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
var displayListGE = ((IEnumerable)DataSource_PI.GetValue(DataConnection_PI.GetValue(productsGrid.productsGrid))).Cast<GridEntry2>();
|
||||||
|
int index = 0;
|
||||||
|
foreach (var di in displayListGE)
|
||||||
|
{
|
||||||
|
di.ListIndex = index++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplayViewModel));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<GridEntry2> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
|
public async Task DisplayBooks(List<LibraryBook> dbBooks)
|
||||||
|
{
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//List is already displayed. Replace all items with new ones, refilter, and re-sort
|
||||||
|
string existingFilter = GridEntries?.Filter;
|
||||||
|
var newEntries = CreateGridEntries(dbBooks);
|
||||||
|
|
||||||
|
var existingSeriesEntries = GridEntries.AllItems().SeriesEntries().ToList();
|
||||||
|
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => GridEntries.ReplaceList(newEntries));
|
||||||
|
|
||||||
|
//We're replacing the list, so preserve usere's existing collapse/expand
|
||||||
|
//state. When resetting a list, default state is open.
|
||||||
|
foreach (var series in existingSeriesEntries)
|
||||||
|
{
|
||||||
|
var sEntry = GridEntries.InternalList.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
|
||||||
|
if (sEntry is SeriesEntrys2 se && !series.Liberate.Expanded)
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => GridEntries.CollapseItem(se));
|
||||||
|
}
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
GridEntries.Filter = existingFilter;
|
||||||
|
ReSort();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplayViewModel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<GridEntry2> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
var geList = dbBooks
|
var geList = dbBooks
|
||||||
.Where(lb => lb.Book.IsProduct())
|
.Where(lb => lb.Book.IsProduct())
|
||||||
@ -50,20 +147,169 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
|
|||||||
}
|
}
|
||||||
return geList.OrderByDescending(e => e.DateAdded);
|
return geList.OrderByDescending(e => e.DateAdded);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
class CustonGroupDescription : DataGridGroupDescription
|
|
||||||
|
public async Task Filter(string searchString)
|
||||||
{
|
{
|
||||||
public override object GroupKeyFromItem(object item, int level, CultureInfo culture)
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
if (item is SeriesEntrys2 sEntry)
|
int visibleCount = GridEntries.Count;
|
||||||
return sEntry;
|
|
||||||
else if (item is LibraryBookEntry2 lbEntry && lbEntry.Parent is SeriesEntrys2 sEntry2)
|
if (string.IsNullOrEmpty(searchString))
|
||||||
return sEntry2;
|
GridEntries.RemoveFilter();
|
||||||
else return null;
|
else
|
||||||
|
GridEntries.Filter = searchString;
|
||||||
|
|
||||||
|
if (visibleCount != GridEntries.Count)
|
||||||
|
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
|
||||||
|
|
||||||
|
//Re-sort after filtering
|
||||||
|
ReSort();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public override bool KeysMatch(object groupKey, object itemKey)
|
|
||||||
|
public void ToggleSeriesExpanded(SeriesEntrys2 seriesEntry)
|
||||||
{
|
{
|
||||||
return base.KeysMatch(groupKey, itemKey);
|
if (seriesEntry.Liberate.Expanded)
|
||||||
|
GridEntries.CollapseItem(seriesEntry);
|
||||||
|
else
|
||||||
|
GridEntries.ExpandItem(seriesEntry);
|
||||||
|
|
||||||
|
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Sort(DataGridColumn sortColumn)
|
||||||
|
{
|
||||||
|
//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 = sortColumn.CustomSortComparer as RowComparer;
|
||||||
|
comparer.SortDirection = null;
|
||||||
|
|
||||||
|
_currentSortColumn = sortColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Must be invoked on UI thread
|
||||||
|
private void ReSort()
|
||||||
|
{
|
||||||
|
if (_currentSortColumn is null)
|
||||||
|
{
|
||||||
|
//Sort ascending and reverse. That's how the comparer is designed to work to be compatible with Avalonia.
|
||||||
|
var defaultComparer = new RowComparer(ListSortDirection.Descending, nameof(GridEntry2.DateAdded));
|
||||||
|
GridEntries.InternalList.Sort(defaultComparer);
|
||||||
|
GridEntries.InternalList.Reverse();
|
||||||
|
GridEntries.ResetCollection();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_currentSortColumn.Sort(((RowComparer)_currentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DoneRemovingBooks()
|
||||||
|
{
|
||||||
|
foreach (var item in GridEntries.AllItems())
|
||||||
|
item.PropertyChanged -= Item_PropertyChanged;
|
||||||
|
RemoveColumnVisivle = 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 = await MessageBox.ShowConfirmationDialog(
|
||||||
|
null,
|
||||||
|
libraryBooks,
|
||||||
|
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
|
||||||
|
"Remove books from Libation?");
|
||||||
|
|
||||||
|
if (result != DialogResult.Yes)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var book in selectedBooks)
|
||||||
|
book.PropertyChanged -= Item_PropertyChanged;
|
||||||
|
|
||||||
|
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
|
||||||
|
GridEntries.CollectionChanged += BindingList_CollectionChanged;
|
||||||
|
|
||||||
|
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
|
||||||
|
//so there's no need to remove books from the grid display here.
|
||||||
|
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
|
||||||
|
|
||||||
|
foreach (var b in GetAllBookEntries())
|
||||||
|
b.Remove = false;
|
||||||
|
|
||||||
|
RemovableCountChanged?.Invoke(this, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//After ProductsDisplay2.Display() re-creates the list,
|
||||||
|
//re-subscribe to all items' PropertyChanged events.
|
||||||
|
|
||||||
|
foreach (var b in GetAllBookEntries())
|
||||||
|
b.PropertyChanged += Item_PropertyChanged;
|
||||||
|
|
||||||
|
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
|
||||||
|
{
|
||||||
|
foreach (var item in GridEntries.AllItems())
|
||||||
|
{
|
||||||
|
item.Remove = false;
|
||||||
|
item.PropertyChanged += Item_PropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveColumnVisivle = true;
|
||||||
|
RemovableCountChanged?.Invoke(this, 0);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(GridEntry2.Remove) && sender is LibraryBookEntry2 lbEntry)
|
||||||
|
{
|
||||||
|
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
|
||||||
|
RemovableCountChanged?.Invoke(this, removeCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
productsDisplay.Filter(filterString);
|
await _viewModel.ProductsDisplay.Filter(filterString);
|
||||||
lastGoodFilter = filterString;
|
lastGoodFilter = filterString;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@ -61,7 +61,7 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
|
|
||||||
public void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => new EditQuickFilters().ShowDialog();
|
public void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => new EditQuickFilters().ShowDialog();
|
||||||
|
|
||||||
public async void productsDisplay_Initialized(object sender, EventArgs e)
|
public async void ProductsDisplay_Initialized(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (QuickFilters.UseDefault)
|
if (QuickFilters.UseDefault)
|
||||||
await performFilter(QuickFilters.Filters.FirstOrDefault());
|
await performFilter(QuickFilters.Filters.FirstOrDefault());
|
||||||
|
|||||||
@ -59,18 +59,18 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
//For removing books within a filter set, use
|
//For removing books within a filter set, use
|
||||||
//Visible Books > Remove from library
|
//Visible Books > Remove from library
|
||||||
|
|
||||||
productsDisplay.Filter(null);
|
await _viewModel.ProductsDisplay.Filter(null);
|
||||||
|
|
||||||
_viewModel.RemoveBooksButtonEnabled = true;
|
_viewModel.RemoveBooksButtonEnabled = true;
|
||||||
_viewModel.RemoveButtonsVisible = true;
|
_viewModel.RemoveButtonsVisible = true;
|
||||||
|
|
||||||
await productsDisplay.ScanAndRemoveBooksAsync(accounts);
|
await _viewModel.ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void removeBooksBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public async void removeBooksBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_viewModel.RemoveBooksButtonEnabled = false;
|
_viewModel.RemoveBooksButtonEnabled = false;
|
||||||
await productsDisplay.RemoveCheckedBooksAsync();
|
await _viewModel.ProductsDisplay.RemoveCheckedBooksAsync();
|
||||||
_viewModel.RemoveBooksButtonEnabled = true;
|
_viewModel.RemoveBooksButtonEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,13 +78,13 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
{
|
{
|
||||||
_viewModel.RemoveButtonsVisible = false;
|
_viewModel.RemoveButtonsVisible = false;
|
||||||
|
|
||||||
productsDisplay.CloseRemoveBooksColumn();
|
_viewModel.ProductsDisplay.DoneRemovingBooks();
|
||||||
|
|
||||||
//Restore the filter
|
//Restore the filter
|
||||||
await performFilter(lastGoodFilter);
|
await performFilter(lastGoodFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void productsDisplay_RemovableCountChanged(object sender, int removeCount)
|
public void ProductsDisplay_RemovableCountChanged(object sender, int removeCount)
|
||||||
{
|
{
|
||||||
_viewModel.RemoveBooksButtonText = removeCount switch
|
_viewModel.RemoveBooksButtonText = removeCount switch
|
||||||
{
|
{
|
||||||
|
|||||||
@ -28,7 +28,8 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
Serilog.Log.Logger.Information("Begin backing up visible library books");
|
Serilog.Log.Logger.Information("Begin backing up visible library books");
|
||||||
|
|
||||||
_viewModel.ProcessQueueViewModel.AddDownloadDecrypt(
|
_viewModel.ProcessQueueViewModel.AddDownloadDecrypt(
|
||||||
productsDisplay
|
_viewModel
|
||||||
|
.ProductsDisplay
|
||||||
.GetVisibleBookEntries()
|
.GetVisibleBookEntries()
|
||||||
.UnLiberated()
|
.UnLiberated()
|
||||||
);
|
);
|
||||||
@ -45,7 +46,7 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
if (result != System.Windows.Forms.DialogResult.OK)
|
if (result != System.Windows.Forms.DialogResult.OK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
|
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
|
||||||
|
|
||||||
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
||||||
this,
|
this,
|
||||||
@ -68,7 +69,7 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
if (result != System.Windows.Forms.DialogResult.OK)
|
if (result != System.Windows.Forms.DialogResult.OK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
|
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
|
||||||
|
|
||||||
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
||||||
this,
|
this,
|
||||||
@ -86,7 +87,7 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
|
|
||||||
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||||
{
|
{
|
||||||
var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
|
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
|
||||||
|
|
||||||
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
||||||
this,
|
this,
|
||||||
@ -100,7 +101,7 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
var visibleIds = visibleLibraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
|
var visibleIds = visibleLibraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
|
||||||
await LibraryCommands.RemoveBooksAsync(visibleIds);
|
await LibraryCommands.RemoveBooksAsync(visibleIds);
|
||||||
}
|
}
|
||||||
public async void productsDisplay_VisibleCountChanged(object sender, int qty)
|
public async void ProductsDisplay_VisibleCountChanged(object sender, int qty)
|
||||||
{
|
{
|
||||||
_viewModel.VisibleCount = qty;
|
_viewModel.VisibleCount = qty;
|
||||||
|
|
||||||
@ -108,7 +109,7 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
}
|
}
|
||||||
void setLiberatedVisibleMenuItem()
|
void setLiberatedVisibleMenuItem()
|
||||||
=> _viewModel.VisibleNotLiberated
|
=> _viewModel.VisibleNotLiberated
|
||||||
= productsDisplay
|
= _viewModel.ProductsDisplay
|
||||||
.GetVisibleBookEntries()
|
.GetVisibleBookEntries()
|
||||||
.Count(lb => lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.NotLiberated);
|
.Count(lb => lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.NotLiberated);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow"
|
x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow"
|
||||||
Title="Libation"
|
Title="Libation"
|
||||||
Name="Form1"
|
Name="Form1"
|
||||||
Icon="/AvaloniaUI/Assets/glass-with-glow_16.png">
|
Icon="/AvaloniaUI/Assets/libation.ico">
|
||||||
|
|
||||||
<Border BorderBrush="{DynamicResource DataGridGridLinesBrush}" BorderThickness="2" Padding="15">
|
<Border BorderBrush="{DynamicResource DataGridGridLinesBrush}" BorderThickness="2" Padding="15">
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||||
@ -173,10 +173,8 @@
|
|||||||
|
|
||||||
<!-- Product Display Grid -->
|
<!-- Product Display Grid -->
|
||||||
<prgid:ProductsDisplay2
|
<prgid:ProductsDisplay2
|
||||||
InitialLoaded="productsDisplay_Initialized"
|
DataContext="{Binding ProductsDisplay}"
|
||||||
LiberateClicked="ProductsDisplay_LiberateClicked"
|
LiberateClicked="ProductsDisplay_LiberateClicked"
|
||||||
RemovableCountChanged="productsDisplay_RemovableCountChanged"
|
|
||||||
VisibleCountChanged="productsDisplay_VisibleCountChanged"
|
|
||||||
Name="productsDisplay" />
|
Name="productsDisplay" />
|
||||||
</SplitView>
|
</SplitView>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@ -2,7 +2,6 @@ using ApplicationServices;
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using LibationWinForms.AvaloniaUI.Controls;
|
|
||||||
using System;
|
using System;
|
||||||
using LibationWinForms.AvaloniaUI.Views.ProductsGrid;
|
using LibationWinForms.AvaloniaUI.Views.ProductsGrid;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
@ -10,6 +9,7 @@ using LibationWinForms.AvaloniaUI.ViewModels;
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace LibationWinForms.AvaloniaUI.Views
|
namespace LibationWinForms.AvaloniaUI.Views
|
||||||
{
|
{
|
||||||
@ -45,13 +45,26 @@ namespace LibationWinForms.AvaloniaUI.Views
|
|||||||
// misc which belongs in winforms app but doesn't have a UI element
|
// misc which belongs in winforms app but doesn't have a UI element
|
||||||
Configure_NonUI();
|
Configure_NonUI();
|
||||||
|
|
||||||
|
_viewModel.ProductsDisplay.InitialLoaded += ProductsDisplay_Initialized;
|
||||||
|
_viewModel.ProductsDisplay.RemovableCountChanged += ProductsDisplay_RemovableCountChanged;
|
||||||
|
_viewModel.ProductsDisplay.VisibleCountChanged += ProductsDisplay_VisibleCountChanged;
|
||||||
|
|
||||||
{
|
{
|
||||||
this.LibraryLoaded += async (_, dbBooks) => await productsDisplay.Display(dbBooks);
|
this.LibraryLoaded += MainWindow_LibraryLoaded;
|
||||||
LibraryCommands.LibrarySizeChanged += async (_, _) => await productsDisplay.Display(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
|
||||||
|
LibraryCommands.LibrarySizeChanged += async (_, _) => await _viewModel.ProductsDisplay.DisplayBooks(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||||
this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance);
|
this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
|
||||||
|
{
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_viewModel.ProductsDisplay.InitialDisplay(dbBooks, productsDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
|||||||
@ -20,16 +20,7 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
|
|||||||
|
|
||||||
if (button.DataContext is SeriesEntrys2 sEntry)
|
if (button.DataContext is SeriesEntrys2 sEntry)
|
||||||
{
|
{
|
||||||
if (sEntry.Liberate.Expanded)
|
_viewModel.ToggleSeriesExpanded(sEntry);
|
||||||
{
|
|
||||||
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
|
//Expanding and collapsing reset the list, which will cause focus to shift
|
||||||
//to the topright cell. Reset focus onto the clicked button's cell.
|
//to the topright cell. Reset focus onto the clicked button's cell.
|
||||||
|
|||||||
@ -8,7 +8,7 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
|
|||||||
{
|
{
|
||||||
public partial class ProductsDisplay2
|
public partial class ProductsDisplay2
|
||||||
{
|
{
|
||||||
ContextMenu contextMenuStrip1 = new ContextMenu();
|
private ContextMenu contextMenuStrip1 = new ContextMenu();
|
||||||
private void Configure_ColumnCustomization()
|
private void Configure_ColumnCustomization()
|
||||||
{
|
{
|
||||||
if (Design.IsDesignMode) return;
|
if (Design.IsDesignMode) return;
|
||||||
@ -68,10 +68,6 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
|
|||||||
|
|
||||||
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, productsGrid.Columns.IndexOf(column));
|
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, productsGrid.Columns.IndexOf(column));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Remove column is always first;
|
|
||||||
removeGVColumn.DisplayIndex = 0;
|
|
||||||
removeGVColumn.CanUserReorder = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ContextMenu_ContextMenuOpening(object sender, System.ComponentModel.CancelEventArgs e)
|
private void ContextMenu_ContextMenuOpening(object sender, System.ComponentModel.CancelEventArgs e)
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using DataLayer;
|
|
||||||
using LibationWinForms.AvaloniaUI.ViewModels;
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
|
|
||||||
{
|
|
||||||
public partial class ProductsDisplay2
|
|
||||||
{
|
|
||||||
private void Configure_Display() { }
|
|
||||||
|
|
||||||
public async Task Display(List<LibraryBook> dbBooks)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_viewModel is null)
|
|
||||||
{
|
|
||||||
_viewModel = new ProductsDisplayViewModel(dbBooks);
|
|
||||||
InitialLoaded?.Invoke(this, EventArgs.Empty);
|
|
||||||
|
|
||||||
int bookEntryCount = bindingList.BookEntries().Count();
|
|
||||||
VisibleCountChanged?.Invoke(this, bookEntryCount);
|
|
||||||
|
|
||||||
//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 each 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
|
|
||||||
//so that out handler executes first.
|
|
||||||
productsGrid.DataContext = _viewModel;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//List is already displayed. Replace all items with new ones, refilter, and re-sort
|
|
||||||
string existingFilter = _viewModel?.GridEntries?.Filter;
|
|
||||||
var newEntries = ProductsDisplayViewModel.CreateGridEntries(dbBooks);
|
|
||||||
|
|
||||||
var existingSeriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
|
||||||
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => bindingList.ReplaceList(newEntries));
|
|
||||||
|
|
||||||
//We're replacing the list, so preserve usere's existing collapse/expand
|
|
||||||
//state. When resetting a list, default state is open.
|
|
||||||
foreach (var series in existingSeriesEntries)
|
|
||||||
{
|
|
||||||
var sEntry = bindingList.InternalList.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
|
|
||||||
if (sEntry is SeriesEntrys2 se && !series.Liberate.Expanded)
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => bindingList.CollapseItem(se));
|
|
||||||
}
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
bindingList.Filter = existingFilter;
|
|
||||||
ReSort();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
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() { }
|
|
||||||
|
|
||||||
private bool RemoveColumnVisible
|
|
||||||
{
|
|
||||||
get => removeGVColumn.IsVisible;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
foreach (var book in bindingList.AllItems())
|
|
||||||
book.Remove = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeGVColumn.DisplayIndex = 0;
|
|
||||||
removeGVColumn.CanUserReorder = value;
|
|
||||||
removeGVColumn.IsVisible = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseRemoveBooksColumn()
|
|
||||||
{
|
|
||||||
RemoveColumnVisible = false;
|
|
||||||
|
|
||||||
foreach (var item in bindingList.AllItems())
|
|
||||||
item.PropertyChanged -= Item_PropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = await MessageBox.ShowConfirmationDialog(
|
|
||||||
null,
|
|
||||||
libraryBooks,
|
|
||||||
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
|
|
||||||
"Remove books from Libation?");
|
|
||||||
|
|
||||||
if (result != DialogResult.Yes)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var book in selectedBooks)
|
|
||||||
book.PropertyChanged -= Item_PropertyChanged;
|
|
||||||
|
|
||||||
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
|
|
||||||
bindingList.CollectionChanged += BindingList_CollectionChanged;
|
|
||||||
|
|
||||||
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
|
|
||||||
//so there's no need to remove books from the grid display here.
|
|
||||||
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
|
|
||||||
|
|
||||||
foreach (var b in GetAllBookEntries())
|
|
||||||
b.Remove = false;
|
|
||||||
|
|
||||||
RemovableCountChanged?.Invoke(this, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
|
|
||||||
return;
|
|
||||||
|
|
||||||
//After ProductsDisplay2.Display() re-creates the list,
|
|
||||||
//re-subscribe to all items' PropertyChanged events.
|
|
||||||
|
|
||||||
foreach (var b in GetAllBookEntries())
|
|
||||||
b.PropertyChanged += Item_PropertyChanged;
|
|
||||||
|
|
||||||
bindingList.CollectionChanged -= BindingList_CollectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
|
|
||||||
{
|
|
||||||
RemovableCountChanged?.Invoke(this, 0);
|
|
||||||
removeGVColumn.IsVisible = true;
|
|
||||||
|
|
||||||
foreach (var item in bindingList.AllItems())
|
|
||||||
item.PropertyChanged += Item_PropertyChanged;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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 Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.PropertyName == nameof(GridEntry2.Remove) && sender is LibraryBookEntry2 lbEntry)
|
|
||||||
{
|
|
||||||
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
|
|
||||||
RemovableCountChanged?.Invoke(this, removeCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using LibationWinForms.AvaloniaUI.ViewModels;
|
|
||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
|
|
||||||
{
|
|
||||||
public partial class ProductsDisplay2
|
|
||||||
{
|
|
||||||
private DataGridColumn CurrentSortColumn;
|
|
||||||
private void Configure_Sorting() { }
|
|
||||||
|
|
||||||
private void ReSort()
|
|
||||||
{
|
|
||||||
if (CurrentSortColumn is null)
|
|
||||||
{
|
|
||||||
//Sort ascending and reverse. That's how the comparer is designed to work to be compatible with Avalonia.
|
|
||||||
var defaultComparer = new RowComparer(ListSortDirection.Descending, nameof(GridEntry2.DateAdded));
|
|
||||||
bindingList.InternalList.Sort(defaultComparer);
|
|
||||||
bindingList.InternalList.Reverse();
|
|
||||||
bindingList.ResetCollection();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CurrentSortColumn.Sort(((RowComparer)CurrentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,11 +13,21 @@
|
|||||||
Name="productsGrid"
|
Name="productsGrid"
|
||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
Items="{Binding GridEntries}"
|
Items="{Binding GridEntries}"
|
||||||
|
Sorting="ProductsGrid_Sorting"
|
||||||
|
CanUserSortColumns="True"
|
||||||
CanUserReorderColumns="True">
|
CanUserReorderColumns="True">
|
||||||
|
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
|
|
||||||
<controls:DataGridCheckBoxColumnExt IsVisible="True" Header="Remove" IsThreeState="True" IsReadOnly="False" CanUserSort="True" Binding="{Binding Remove, Mode=TwoWay}" Width="70" SortMemberPath="Remove" />
|
<controls:DataGridCheckBoxColumnExt
|
||||||
|
PropertyChanged="RemoveColumn_PropertyChanged"
|
||||||
|
IsVisible="{Binding RemoveColumnVisivle}"
|
||||||
|
Header="Remove"
|
||||||
|
IsThreeState="True"
|
||||||
|
IsReadOnly="False"
|
||||||
|
CanUserSort="True"
|
||||||
|
Binding="{Binding Remove, Mode=TwoWay}"
|
||||||
|
Width="70" SortMemberPath="Remove" />
|
||||||
|
|
||||||
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate">
|
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
|||||||
@ -1,38 +1,17 @@
|
|||||||
using ApplicationServices;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media;
|
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using LibationWinForms.AvaloniaUI.ViewModels;
|
using LibationWinForms.AvaloniaUI.ViewModels;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
|
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
|
||||||
{
|
{
|
||||||
public partial class ProductsDisplay2 : UserControl
|
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<LibraryBook> LiberateClicked;
|
||||||
public event EventHandler InitialLoaded;
|
|
||||||
|
|
||||||
|
private ProductsDisplayViewModel _viewModel => DataContext as ProductsDisplayViewModel;
|
||||||
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;
|
|
||||||
|
|
||||||
public ProductsDisplay2()
|
public ProductsDisplay2()
|
||||||
{
|
{
|
||||||
@ -40,34 +19,32 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
|
|||||||
|
|
||||||
Configure_Buttons();
|
Configure_Buttons();
|
||||||
Configure_ColumnCustomization();
|
Configure_ColumnCustomization();
|
||||||
Configure_Display();
|
|
||||||
Configure_Filtering();
|
|
||||||
Configure_ScanAndRemove();
|
|
||||||
Configure_Sorting();
|
|
||||||
|
|
||||||
foreach (var column in productsGrid.Columns)
|
foreach (var column in productsGrid.Columns)
|
||||||
{
|
{
|
||||||
column.CustomSortComparer = new RowComparer(column);
|
column.CustomSortComparer = new RowComparer(column);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Design.IsDesignMode)
|
private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
|
||||||
{
|
{
|
||||||
using var context = DbContexts.GetContext();
|
_viewModel.Sort(e.Column);
|
||||||
var book = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
|
|
||||||
productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(new List<LibraryBook> { book });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RemoveColumn_PropertyChanged(object sender, Avalonia.AvaloniaPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is DataGridColumn col && e.Property.Name == nameof(DataGridColumn.IsVisible))
|
||||||
|
{
|
||||||
|
col.DisplayIndex = 0;
|
||||||
|
col.CanUserReorder = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
productsGrid = this.FindControl<DataGrid>(nameof(productsGrid));
|
productsGrid = this.FindControl<DataGrid>(nameof(productsGrid));
|
||||||
productsGrid.Sorting += ProductsGrid_Sorting;
|
|
||||||
productsGrid.CanUserSortColumns = true;
|
|
||||||
|
|
||||||
removeGVColumn = productsGrid.Columns[0];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
<None Remove="AvaloniaUI\Assets\glass-with-glow_16.png" />
|
<None Remove="AvaloniaUI\Assets\glass-with-glow_16.png" />
|
||||||
<None Remove="AvaloniaUI\Assets\import_16x16.png" />
|
<None Remove="AvaloniaUI\Assets\import_16x16.png" />
|
||||||
<None Remove="AvaloniaUI\Assets\last.png" />
|
<None Remove="AvaloniaUI\Assets\last.png" />
|
||||||
|
<None Remove="AvaloniaUI\Assets\libation.ico" />
|
||||||
<None Remove="AvaloniaUI\Assets\LibationStyles.xaml" />
|
<None Remove="AvaloniaUI\Assets\LibationStyles.xaml" />
|
||||||
<None Remove="AvaloniaUI\Assets\MBIcons\Asterisk.png" />
|
<None Remove="AvaloniaUI\Assets\MBIcons\Asterisk.png" />
|
||||||
<None Remove="AvaloniaUI\Assets\MBIcons\error.png" />
|
<None Remove="AvaloniaUI\Assets\MBIcons\error.png" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user