From 49c6b391fdce70d1f9dd9d95f68f59cfea9041c9 Mon Sep 17 00:00:00 2001
From: MBucari
Date: Fri, 10 Mar 2023 19:37:42 -0700
Subject: [PATCH] WinForms and Avalonia now share all GridEntry view models
---
.../Controls/DataGridCheckBoxColumnExt.cs | 8 +-
.../Controls/DataGridContextMenus.cs | 6 +-
.../Controls/DataGridMyRatingColumn.cs | 25 +-
.../ViewModels/AvaloniaEntryStatus.cs | 37 ++
.../LibationAvalonia/ViewModels/BookTags.cs | 9 -
.../LibationAvalonia/ViewModels/GridEntry.cs | 238 -------------
.../ViewModels/LibraryBookEntry.cs | 170 ---------
.../ViewModels/ProductsDisplayViewModel.cs | 68 ++--
.../ViewModels/RowComparer.cs | 37 +-
.../ViewModels/SeriesEntry.cs | 135 --------
.../Views/ProductsDisplay.axaml | 54 ++-
.../Views/ProductsDisplay.axaml.cs | 89 ++---
.../GridView/EntryStatus.cs} | 96 ++++--
.../GridView/GridEntry[TStatus].cs | 324 ++++++++++++++++++
Source/LibationUiBase/GridView/IGridEntry.cs | 34 ++
.../GridView/ILibraryBookEntry.cs | 7 +
.../LibationUiBase/GridView/ISeriesEntry.cs | 11 +
.../{ => GridView}/LastDownloadStatus.cs | 2 +-
.../GridView/LibraryBookEntry[TStatus].cs | 34 ++
.../{ => GridView}/ObjectComparer[T].cs | 2 +-
.../GridView}/QueryExtensions.cs | 18 +-
.../GridView/SeriesEntry[TStatus].cs | 68 ++++
Source/LibationUiBase/IcoEncoder.cs | 6 +-
Source/LibationUiBase/SampleRateSelection.cs | 8 +-
Source/LibationUiBase/TrackedQueue[T].cs | 4 +-
Source/LibationUiBase/Upgrader.cs | 2 +-
.../EditTagsDataGridViewImageButtonColumn.cs | 23 +-
Source/LibationWinForms/GridView/GridEntry.cs | 233 -------------
.../GridView/GridEntryBindingList.cs | 31 +-
.../GridView/LastDownloadedGridViewColumn.cs | 5 +-
.../GridView/LiberateButtonStatus.cs | 38 --
.../LiberateDataGridViewImageButtonColumn.cs | 72 +---
.../GridView/LibraryBookEntry.cs | 177 ----------
.../GridView/MyRatingGridViewColumn.cs | 4 +-
.../GridView/ProductsDisplay.cs | 23 +-
.../GridView/ProductsGrid.Designer.cs | 15 +-
.../LibationWinForms/GridView/ProductsGrid.cs | 210 ++++++------
.../GridView/QueryExtensions.cs | 44 ---
.../LibationWinForms/GridView/SeriesEntry.cs | 133 -------
.../GridView/WinFormsEntryStatus.cs | 37 ++
40 files changed, 930 insertions(+), 1607 deletions(-)
create mode 100644 Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs
delete mode 100644 Source/LibationAvalonia/ViewModels/BookTags.cs
delete mode 100644 Source/LibationAvalonia/ViewModels/GridEntry.cs
delete mode 100644 Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs
delete mode 100644 Source/LibationAvalonia/ViewModels/SeriesEntry.cs
rename Source/{LibationAvalonia/ViewModels/LiberateButtonStatus.cs => LibationUiBase/GridView/EntryStatus.cs} (50%)
create mode 100644 Source/LibationUiBase/GridView/GridEntry[TStatus].cs
create mode 100644 Source/LibationUiBase/GridView/IGridEntry.cs
create mode 100644 Source/LibationUiBase/GridView/ILibraryBookEntry.cs
create mode 100644 Source/LibationUiBase/GridView/ISeriesEntry.cs
rename Source/LibationUiBase/{ => GridView}/LastDownloadStatus.cs (97%)
create mode 100644 Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs
rename Source/LibationUiBase/{ => GridView}/ObjectComparer[T].cs (84%)
rename Source/{LibationAvalonia/ViewModels => LibationUiBase/GridView}/QueryExtensions.cs (59%)
create mode 100644 Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs
delete mode 100644 Source/LibationWinForms/GridView/GridEntry.cs
delete mode 100644 Source/LibationWinForms/GridView/LiberateButtonStatus.cs
delete mode 100644 Source/LibationWinForms/GridView/LibraryBookEntry.cs
delete mode 100644 Source/LibationWinForms/GridView/QueryExtensions.cs
delete mode 100644 Source/LibationWinForms/GridView/SeriesEntry.cs
create mode 100644 Source/LibationWinForms/GridView/WinFormsEntryStatus.cs
diff --git a/Source/LibationAvalonia/Controls/DataGridCheckBoxColumnExt.cs b/Source/LibationAvalonia/Controls/DataGridCheckBoxColumnExt.cs
index 0bf914c9..de44df9a 100644
--- a/Source/LibationAvalonia/Controls/DataGridCheckBoxColumnExt.cs
+++ b/Source/LibationAvalonia/Controls/DataGridCheckBoxColumnExt.cs
@@ -1,17 +1,15 @@
using Avalonia.Controls;
-using LibationAvalonia.ViewModels;
-using System;
-using System.Linq;
+using LibationUiBase.GridView;
namespace LibationAvalonia.Controls
{
- public class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
+ public class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
{
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.IsThreeState = dataItem is SeriesEntry;
+ ele.IsThreeState = dataItem is ISeriesEntry;
return ele;
}
}
diff --git a/Source/LibationAvalonia/Controls/DataGridContextMenus.cs b/Source/LibationAvalonia/Controls/DataGridContextMenus.cs
index 68660430..277e68ee 100644
--- a/Source/LibationAvalonia/Controls/DataGridContextMenus.cs
+++ b/Source/LibationAvalonia/Controls/DataGridContextMenus.cs
@@ -1,6 +1,6 @@
using Avalonia.Collections;
using Avalonia.Controls;
-using LibationAvalonia.ViewModels;
+using LibationUiBase.GridView;
using System;
using System.Reflection;
@@ -30,7 +30,7 @@ namespace LibationAvalonia.Controls
private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e)
{
- if (sender is DataGridCell cell && cell.DataContext is GridEntry entry)
+ if (sender is DataGridCell cell && cell.DataContext is IGridEntry entry)
{
var args = new DataGridCellContextMenuStripNeededEventArgs
{
@@ -63,7 +63,7 @@ namespace LibationAvalonia.Controls
public string CellClipboardContents => GetCellValue(Column, GridEntry);
public DataGridColumn Column { get; init; }
- public GridEntry GridEntry { get; init; }
+ public IGridEntry GridEntry { get; init; }
public ContextMenu ContextMenu { get; init; }
public AvaloniaList ContextMenuItems
=> ContextMenu.Items as AvaloniaList;
diff --git a/Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs b/Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs
index b563883e..5a655ceb 100644
--- a/Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs
+++ b/Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs
@@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Data;
using Avalonia.Interactivity;
using DataLayer;
using ReactiveUI;
@@ -7,19 +8,10 @@ using System;
namespace LibationAvalonia.Controls
{
- public class StarStringConverter : Avalonia.Data.Converters.IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
- => value is Rating rating ? rating.ToStarString() : string.Empty;
-
- public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
- => throw new NotImplementedException();
- }
-
public class DataGridMyRatingColumn : DataGridBoundColumn
{
- [Avalonia.Data.AssignBinding]
- public Avalonia.Data.IBinding BackgroundBinding { get; set; }
+ [AssignBinding] public IBinding BackgroundBinding { get; set; }
+ [AssignBinding] public IBinding OpacityBinding { get; set; }
private static Rating DefaultRating => new Rating(0, 0, 0);
public DataGridMyRatingColumn()
{
@@ -40,13 +32,11 @@ namespace LibationAvalonia.Controls
ToolTip.SetTip(myRatingElement, "Click to change ratings");
if (Binding != null)
- {
myRatingElement.Bind(BindingTarget, Binding);
- }
if (BackgroundBinding != null)
- {
myRatingElement.Bind(MyRatingCellEditor.BackgroundProperty, BackgroundBinding);
- }
+ if (OpacityBinding != null)
+ myRatingElement.Bind(MyRatingCellEditor.OpacityProperty, OpacityBinding);
return myRatingElement;
}
@@ -58,10 +48,11 @@ namespace LibationAvalonia.Controls
Name = "CellMyRatingEditor",
IsEditingMode = true
};
+
if (BackgroundBinding != null)
- {
myRatingElement.Bind(MyRatingCellEditor.BackgroundProperty, BackgroundBinding);
- }
+ if (OpacityBinding != null)
+ myRatingElement.Bind(MyRatingCellEditor.OpacityProperty, OpacityBinding);
return myRatingElement;
}
diff --git a/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs b/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs
new file mode 100644
index 00000000..e484206c
--- /dev/null
+++ b/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs
@@ -0,0 +1,37 @@
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using DataLayer;
+using LibationUiBase.GridView;
+using System;
+
+namespace LibationAvalonia.ViewModels
+{
+ public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
+ {
+ private static Bitmap _defaultImage;
+ public override IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;
+
+ private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
+ public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook);
+
+ protected override Bitmap LoadImage(byte[] picture)
+ {
+ try
+ {
+ using var ms = new System.IO.MemoryStream(picture);
+ return new Bitmap(ms);
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Logger.Error(ex, "Error loading cover art for {Book}", Book);
+ return _defaultImage ??= new Bitmap(App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg"));
+ }
+ }
+
+ protected override Bitmap GetResourceImage(string rescName)
+ {
+ using var stream = App.OpenAsset(rescName + ".png");
+ return new Bitmap(stream);
+ }
+ }
+}
diff --git a/Source/LibationAvalonia/ViewModels/BookTags.cs b/Source/LibationAvalonia/ViewModels/BookTags.cs
deleted file mode 100644
index 0f24941d..00000000
--- a/Source/LibationAvalonia/ViewModels/BookTags.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace LibationAvalonia.ViewModels
-{
- public class BookTags
- {
- private string _tags;
- public string Tags { get => _tags; init { _tags = value; HasTags = !string.IsNullOrEmpty(_tags); } }
- public bool HasTags { get; init; }
- }
-}
diff --git a/Source/LibationAvalonia/ViewModels/GridEntry.cs b/Source/LibationAvalonia/ViewModels/GridEntry.cs
deleted file mode 100644
index 2b989afd..00000000
--- a/Source/LibationAvalonia/ViewModels/GridEntry.cs
+++ /dev/null
@@ -1,238 +0,0 @@
-using ApplicationServices;
-using Avalonia.Media;
-using Avalonia.Media.Imaging;
-using DataLayer;
-using Dinah.Core;
-using FileLiberator;
-using LibationFileManager;
-using LibationUiBase;
-using ReactiveUI;
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace LibationAvalonia.ViewModels
-{
- public enum RemoveStatus
- {
- NotRemoved,
- Removed,
- SomeRemoved
- }
- /// The View Model base for the DataGridView
- public abstract class GridEntry : ViewModelBase
- {
- [Browsable(false)] public string AudibleProductId => Book.AudibleProductId;
- [Browsable(false)] public LibraryBook LibraryBook { get; protected set; }
- [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)] public 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 LastDownloadStatus _lastDownload;
- private string _description;
- private Rating _productrating;
- protected Rating _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 LastDownloadStatus LastDownload { get => _lastDownload; protected set => this.RaiseAndSetIfChanged(ref _lastDownload, 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 Rating ProductRating { get => _productrating; protected set => this.RaiseAndSetIfChanged(ref _productrating, value); }
- public Rating MyRating
- {
- get => _myRating;
- set
- {
- if (_myRating != value
- && value.OverallRating != 0
- && updateReviewTask?.IsCompleted is not false)
- {
- updateReviewTask = UpdateRating(value);
- }
- }
- }
-
- protected bool? _remove = false;
- public abstract bool? Remove { get; set; }
- public abstract LiberateButtonStatus Liberate { get; }
- public abstract BookTags BookTags { get; }
- public abstract bool IsEpisode { get; }
- public abstract bool IsBook { get; }
- public IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;
-
- #endregion
-
- #region User rating
-
- private Task updateReviewTask;
- private async Task UpdateRating(Rating rating)
- {
- var api = await LibraryBook.GetApiAsync();
-
- if (await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating))
- {
- _myRating = rating;
- LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating);
- }
-
- this.RaisePropertyChanged(nameof(MyRating));
- }
-
- #endregion
-
- #region Sorting
-
- public GridEntry() => _memberValues = CreateMemberValueDictionary();
-
- // These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
- // Used by GridEntryBindingList for all sorting
- public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
- public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
- protected abstract Dictionary> CreateMemberValueDictionary();
- private Dictionary> _memberValues { get; set; }
-
- // Instantiate comparers for every exposed member object type.
- private static readonly Dictionary _memberTypeComparers = new()
- {
- { typeof(RemoveStatus), new ObjectComparer() },
- { typeof(string), new ObjectComparer() },
- { typeof(int), new ObjectComparer() },
- { typeof(float), new ObjectComparer() },
- { typeof(bool), new ObjectComparer() },
- { typeof(DateTime), new ObjectComparer() },
- { typeof(LiberateButtonStatus), new ObjectComparer() },
- { typeof(LastDownloadStatus), new ObjectComparer() },
- };
-
- #endregion
-
- #region Cover Art
-
- protected void LoadCover()
- {
- // Get cover art. If it's default, subscribe to PictureCached
- (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
-
- if (isDefault)
- PictureStorage.PictureCached += PictureStorage_PictureCached;
-
- // Mutable property. Set the field so PropertyChanged isn't fired.
- _cover = loadImage(picture);
- }
-
- private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
- {
- // state validation
- if (e is null ||
- e.Definition.PictureId is null ||
- Book?.PictureId is null ||
- e.Picture is null ||
- e.Picture.Length == 0)
- return;
-
- // logic validation
- if (e.Definition.PictureId == Book.PictureId)
- {
- Cover = loadImage(e.Picture);
- PictureStorage.PictureCached -= PictureStorage_PictureCached;
- }
- }
-
- private Bitmap loadImage(byte[] picture)
- {
- try
- {
- using var ms = new System.IO.MemoryStream(picture);
- return new Bitmap(ms);
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, "Error loading cover art for {Book}", Book);
- return DefaultImage;
- }
- }
-
- private static Bitmap _defaultImage;
- private static Bitmap DefaultImage => _defaultImage ??= new Bitmap(App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg"));
-
- #endregion
-
- #region Static library display functions
-
- /// This information should not change during lifetime, so call only once.
- protected static string GetDescriptionDisplay(Book book)
- {
- var doc = new HtmlAgilityPack.HtmlDocument();
- doc.LoadHtml(book?.Description?.Replace("
", "\r\n\r\n") ?? "");
- return doc.DocumentNode.InnerText.Trim();
- }
-
- protected static string TrimTextToWord(string text, int maxLength)
- {
- return
- text.Length <= maxLength ?
- text :
- text.Substring(0, maxLength - 3) + "...";
- }
-
-
- ///
- /// This information should not change during lifetime, so call only once.
- /// Maximum of 5 text rows will fit in 80-pixel row height.
- ///
- protected static string GetMiscDisplay(LibraryBook libraryBook)
- {
- var details = new List();
-
- var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]");
- var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]");
-
- details.Add($"Account: {locale} - {acct}");
-
- if (libraryBook.Book.HasPdf())
- details.Add("Has PDF");
- if (libraryBook.Book.IsAbridged)
- details.Add("Abridged");
- if (libraryBook.Book.DatePublished.HasValue)
- details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}");
- // this goes last since it's most likely to have a line-break
- if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher))
- details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}");
-
- if (!details.Any())
- return "[details not imported]";
-
- return string.Join("\r\n", details);
- }
-
- #endregion
-
- ~GridEntry()
- {
- PictureStorage.PictureCached -= PictureStorage_PictureCached;
- }
- }
-}
diff --git a/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs b/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs
deleted file mode 100644
index 20ac780f..00000000
--- a/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-using ApplicationServices;
-using DataLayer;
-using Dinah.Core;
-using ReactiveUI;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-
-namespace LibationAvalonia.ViewModels
-{
- /// The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode
- public class LibraryBookEntry : GridEntry
- {
- [Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded;
- [Browsable(false)] public SeriesEntry Parent { get; init; }
-
- #region Model properties exposed to the view
-
- private DateTime lastStatusUpdate = default;
- private LiberatedStatus _bookStatus;
- private LiberatedStatus? _pdfStatus;
-
- public override bool? Remove
- {
- get => _remove;
- set
- {
- _remove = value ?? false;
-
- Parent?.ChildRemoveUpdate();
- this.RaisePropertyChanged(nameof(Remove));
- }
- }
-
- public override LiberateButtonStatus Liberate
- {
- get
- {
- //Cache these statuses for faster sorting.
- if ((DateTime.Now - lastStatusUpdate).TotalSeconds > 2)
- {
- _bookStatus = LibraryCommands.Liberated_Status(LibraryBook.Book);
- _pdfStatus = LibraryCommands.Pdf_Status(LibraryBook.Book);
- lastStatusUpdate = DateTime.Now;
- }
- return new LiberateButtonStatus(isSeries: false, LibraryBook.AbsentFromLastScan) { BookStatus = _bookStatus, PdfStatus = _pdfStatus };
- }
- }
-
- public override BookTags BookTags => new() { Tags = string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated) };
-
- public override bool IsEpisode => Parent is not null;
- public override bool IsBook => Parent is null;
-
- #endregion
-
- public LibraryBookEntry(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;
-
- Title = Book.Title;
- Series = Book.SeriesNames();
- Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
- //Ratings are changed using Update(), which is a problem for Avalonia data bindings because
- //the reference doesn't change. Clone the rating so that it updates within Avalonia properly.
- _myRating = new Rating(Book.UserDefinedItem.Rating.OverallRating, Book.UserDefinedItem.Rating.PerformanceRating, Book.UserDefinedItem.Rating.StoryRating);
- PurchaseDate = libraryBook.DateAdded.ToString("d");
- ProductRating = Book.Rating ?? new Rating(0, 0, 0);
- Authors = Book.AuthorNames();
- Narrators = Book.NarratorNames();
- Category = string.Join(" > ", Book.CategoriesNames());
- Misc = GetMiscDisplay(libraryBook);
- LastDownload = new(Book.UserDefinedItem);
- LongDescription = GetDescriptionDisplay(Book);
- Description = TrimTextToWord(LongDescription, 62);
- SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
-
- this.RaisePropertyChanged(nameof(MyRating));
- this.RaisePropertyChanged(nameof(Liberate));
- UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
- }
-
- #region detect changes to the model, update the view.
-
- ///
- /// This event handler receives notifications from the model that it has changed.
- /// Notify the view that it's changed.
- ///
- private void UserDefinedItem_ItemChanged(object sender, string itemName)
- {
- var udi = sender as UserDefinedItem;
-
- if (udi.Book.AudibleProductId != Book.AudibleProductId)
- return;
-
- // UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
- // - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs
- // - Don't restrict notifying view to 'only if property changed'. This same book instance can get passed to a different view, then changed there. When the chain of events makes its way back here, the property is unchanged (because it's the same instance), but this view is out of sync. NotifyPropertyChanged will then update this view.
- switch (itemName)
- {
- case nameof(udi.Tags):
- Book.UserDefinedItem.Tags = udi.Tags;
- this.RaisePropertyChanged(nameof(BookTags));
- break;
- case nameof(udi.BookStatus):
- Book.UserDefinedItem.BookStatus = udi.BookStatus;
- _bookStatus = udi.BookStatus;
- this.RaisePropertyChanged(nameof(Liberate));
- break;
- case nameof(udi.PdfStatus):
- Book.UserDefinedItem.SetPdfStatus(udi.PdfStatus);
- _pdfStatus = udi.PdfStatus;
- this.RaisePropertyChanged(nameof(Liberate));
- break;
- case nameof(udi.LastDownloaded):
- LastDownload = new(udi);
- this.RaisePropertyChanged(nameof(LastDownload));
- break;
- }
- }
-
- #endregion
-
- #region Data Sorting
-
- /// Create getters for all member object values by name
- protected override Dictionary> CreateMemberValueDictionary() => new()
- {
- { nameof(Remove), () => Remove.HasValue ? Remove.Value ? RemoveStatus.Removed : RemoveStatus.NotRemoved : RemoveStatus.SomeRemoved },
- { nameof(Title), () => Book.TitleSortable() },
- { nameof(Series), () => Book.SeriesSortable() },
- { nameof(Length), () => Book.LengthInMinutes },
- { nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore() },
- { nameof(PurchaseDate), () => LibraryBook.DateAdded },
- { nameof(ProductRating), () => Book.Rating.FirstScore() },
- { nameof(Authors), () => Authors },
- { nameof(Narrators), () => Narrators },
- { nameof(Description), () => Description },
- { nameof(Category), () => Category },
- { nameof(Misc), () => Misc },
- { nameof(LastDownload), () => LastDownload },
- { nameof(BookTags), () => BookTags?.Tags ?? string.Empty },
- { nameof(Liberate), () => Liberate },
- { nameof(DateAdded), () => DateAdded },
- };
-
- #endregion
-
- ~LibraryBookEntry()
- {
- UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
- }
- }
-}
diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs
index 601b53fc..8307a48b 100644
--- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs
@@ -1,15 +1,16 @@
+using ApplicationServices;
+using AudibleUtilities;
+using Avalonia.Collections;
+using Avalonia.Threading;
using DataLayer;
+using LibationAvalonia.Dialogs.Login;
+using LibationUiBase.GridView;
+using ReactiveUI;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
-using ReactiveUI;
-using Avalonia.Threading;
-using ApplicationServices;
-using AudibleUtilities;
-using LibationAvalonia.Dialogs.Login;
-using Avalonia.Collections;
namespace LibationAvalonia.ViewModels
{
@@ -20,9 +21,9 @@ namespace LibationAvalonia.ViewModels
public event EventHandler RemovableCountChanged;
/// Backing list of all grid entries
- private readonly AvaloniaList SOURCE = new();
+ private readonly AvaloniaList SOURCE = new();
/// Grid entries included in the filter set. If null, all grid entries are shown
- private List FilteredInGridEntries;
+ private List FilteredInGridEntries;
public string FilterString { get; private set; }
public DataGridCollectionView GridEntries { get; private set; }
@@ -31,11 +32,11 @@ namespace LibationAvalonia.ViewModels
public List GetVisibleBookEntries()
=> GridEntries
- .OfType()
+ .OfType()
.Select(lbe => lbe.LibraryBook)
.ToList();
- private IEnumerable GetAllBookEntries()
+ private IEnumerable GetAllBookEntries()
=> SOURCE
.BookEntries();
@@ -92,11 +93,10 @@ namespace LibationAvalonia.ViewModels
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
- .Select(b => new LibraryBookEntry(b))
- .Cast()
- .ToList();
+ .Select(b => new LibraryBookEntry(b))
+ .ToList();
- var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
+ var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild()).ToList();
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
@@ -106,7 +106,7 @@ namespace LibationAvalonia.ViewModels
if (!seriesEpisodes.Any()) continue;
- var seriesEntry = new SeriesEntry(parent, seriesEpisodes);
+ var seriesEntry = new SeriesEntry(parent, seriesEpisodes);
seriesEntry.Liberate.Expanded = false;
geList.Add(seriesEntry);
@@ -116,9 +116,10 @@ namespace LibationAvalonia.ViewModels
//Create the filtered-in list before adding entries to avoid a refresh
FilteredInGridEntries = QueryResults(geList, FilterString);
SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded));
- VisibleCountChanged?.Invoke(this, GridEntries.OfType().Count());
GridEntries.CollectionChanged += (_, _)
- => VisibleCountChanged?.Invoke(this, GridEntries.OfType().Count());
+ => VisibleCountChanged?.Invoke(this, GridEntries.OfType().Count());
+
+ VisibleCountChanged?.Invoke(this, GridEntries.OfType().Count());
}
///
@@ -129,7 +130,7 @@ namespace LibationAvalonia.ViewModels
#region Add new or update existing grid entries
//Add absent entries to grid, or update existing entry
- var allEntries = SOURCE.BookEntries();
+ var allEntries = SOURCE.BookEntries().ToList();
var seriesEntries = SOURCE.SeriesEntries().ToList();
var parentedEpisodes = dbBooks.ParentedEpisodes().ToList();
@@ -163,7 +164,7 @@ namespace LibationAvalonia.ViewModels
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
//Remove books in series from their parents' Children list
- foreach (var removed in removedBooks.Where(b => b.Parent is not null))
+ foreach (var removed in removedBooks.Where(b => b.Liberate.IsEpisode))
removed.Parent.RemoveChild(removed);
//Remove series that have no children
@@ -174,11 +175,12 @@ namespace LibationAvalonia.ViewModels
#endregion
await Filter(FilterString);
+ GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
}
- private void RemoveBooks(IEnumerable removedBooks, IEnumerable removedSeries)
+ private void RemoveBooks(IEnumerable removedBooks, IEnumerable removedSeries)
{
- foreach (var removed in removedBooks.Cast().Concat(removedSeries).Where(b => b is not null).ToList())
+ foreach (var removed in removedBooks.Cast().Concat(removedSeries).Where(b => b is not null).ToList())
{
if (GridEntries.PassesFilter(removed))
GridEntries.Remove(removed);
@@ -191,21 +193,21 @@ namespace LibationAvalonia.ViewModels
}
}
- private void UpsertBook(LibraryBook book, LibraryBookEntry existingBookEntry)
+ private void UpsertBook(LibraryBook book, ILibraryBookEntry existingBookEntry)
{
if (existingBookEntry is null)
// Add the new product to top
- SOURCE.Insert(0, new LibraryBookEntry(book));
+ SOURCE.Insert(0, new LibraryBookEntry(book));
else
// update existing
existingBookEntry.UpdateLibraryBook(book);
}
- private void UpsertEpisode(LibraryBook episodeBook, LibraryBookEntry existingEpisodeEntry, List seriesEntries, IEnumerable dbBooks)
+ private void UpsertEpisode(LibraryBook episodeBook, ILibraryBookEntry existingEpisodeEntry, List seriesEntries, IEnumerable dbBooks)
{
if (existingEpisodeEntry is null)
{
- LibraryBookEntry episodeEntry;
+ ILibraryBookEntry episodeEntry;
var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
@@ -223,7 +225,7 @@ namespace LibationAvalonia.ViewModels
return;
}
- seriesEntry = new SeriesEntry(seriesBook, new[] { episodeBook });
+ seriesEntry = new SeriesEntry(seriesBook, episodeBook);
seriesEntries.Add(seriesEntry);
episodeEntry = seriesEntry.Children[0];
@@ -233,7 +235,7 @@ namespace LibationAvalonia.ViewModels
else
{
//Series exists. Create and add episode child then update the SeriesEntry
- episodeEntry = new(episodeBook) { Parent = seriesEntry };
+ episodeEntry = new LibraryBookEntry(episodeBook, seriesEntry);
seriesEntry.Children.Add(episodeEntry);
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
seriesEntry.UpdateLibraryBook(seriesBook);
@@ -255,7 +257,7 @@ namespace LibationAvalonia.ViewModels
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
}
- public async Task ToggleSeriesExpanded(SeriesEntry seriesEntry)
+ public async Task ToggleSeriesExpanded(ISeriesEntry seriesEntry)
{
seriesEntry.Liberate.Expanded = !seriesEntry.Liberate.Expanded;
@@ -280,8 +282,8 @@ namespace LibationAvalonia.ViewModels
private bool CollectionFilter(object item)
{
- if (item is LibraryBookEntry lbe
- && lbe.IsEpisode
+ if (item is ILibraryBookEntry lbe
+ && lbe.Liberate.IsEpisode
&& lbe.Parent?.Liberate?.Expanded != true)
return false;
@@ -290,13 +292,13 @@ namespace LibationAvalonia.ViewModels
return FilteredInGridEntries.Contains(item);
}
- private static List QueryResults(IEnumerable entries, string searchString)
+ private static List QueryResults(IEnumerable entries, string searchString)
{
if (string.IsNullOrEmpty(searchString)) return null;
var searchResultSet = SearchEngineCommands.Search(searchString);
- var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
+ var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
//Find all series containing children that match the search criteria
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
@@ -411,7 +413,7 @@ namespace LibationAvalonia.ViewModels
private void GridEntry_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName == nameof(GridEntry.Remove) && sender is LibraryBookEntry lbEntry)
+ if (e.PropertyName == nameof(IGridEntry.Remove) && sender is ILibraryBookEntry)
{
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
RemovableCountChanged?.Invoke(this, removeCount);
diff --git a/Source/LibationAvalonia/ViewModels/RowComparer.cs b/Source/LibationAvalonia/ViewModels/RowComparer.cs
index 71a912ee..b20dc5de 100644
--- a/Source/LibationAvalonia/ViewModels/RowComparer.cs
+++ b/Source/LibationAvalonia/ViewModels/RowComparer.cs
@@ -1,5 +1,5 @@
using Avalonia.Controls;
-using System;
+using LibationUiBase.GridView;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
@@ -13,7 +13,7 @@ namespace LibationAvalonia.ViewModels
/// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
/// properties when 2 items compare equal.
///
- internal class RowComparer : IComparer, IComparer, IComparer