WinForms and Avalonia now share all GridEntry view models

This commit is contained in:
MBucari 2023-03-10 19:37:42 -07:00
parent e1cd8b8f94
commit fb9d062545
41 changed files with 1032 additions and 1704 deletions

View file

@ -1,10 +1,11 @@
using System.Drawing;
using System.Windows.Forms;
using Dinah.Core.WindowsDesktop.Forms;
using LibationUiBase.GridView;
namespace LibationWinForms.GridView
{
public class EditTagsDataGridViewImageButtonColumn : DataGridViewButtonColumn
public class EditTagsDataGridViewImageButtonColumn : DataGridViewButtonColumn
{
public EditTagsDataGridViewImageButtonColumn()
{
@ -15,34 +16,18 @@ namespace LibationWinForms.GridView
internal class EditTagsDataGridViewImageButtonCell : DataGridViewImageButtonCell
{
private static Image ButtonImage { get; } = Properties.Resources.edit_25x25;
private static Color HiddenForeColor { get; } = Color.LightGray;
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
if (rowIndex >= 0 && DataGridView.GetBoundItem<GridEntry>(rowIndex) is SeriesEntry)
{
if (rowIndex >= 0 && DataGridView.GetBoundItem<IGridEntry>(rowIndex) is ISeriesEntry)
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, DataGridViewPaintParts.Background | DataGridViewPaintParts.Border);
return;
}
var tagsString = (string)value;
var foreColor = tagsString?.Contains("hidden") == true ? HiddenForeColor : DataGridView.DefaultCellStyle.ForeColor;
if (DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor != foreColor)
{
DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor = foreColor;
}
if (tagsString?.Length == 0)
else if (value is string tagStr && tagStr.Length == 0)
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
DrawButtonImage(graphics, ButtonImage, cellBounds);
}
else
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);
}
}
}
}

View file

@ -1,233 +0,0 @@
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using Dinah.Core.DataBinding;
using Dinah.Core.WindowsDesktop.Drawing;
using FileLiberator;
using LibationFileManager;
using LibationUiBase;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.GridView
{
public enum RemoveStatus
{
NotRemoved,
Removed,
SomeRemoved
}
/// <summary>The View Model base for the DataGridView</summary>
public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
{
[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 Book Book => LibraryBook.Book;
[Browsable(false)] public abstract bool IsSeries { get; }
[Browsable(false)] public abstract bool IsEpisode { get; }
[Browsable(false)] public abstract bool IsBook { get; }
#region Model properties exposed to the view
protected RemoveStatus _remove = RemoveStatus.NotRemoved;
public abstract RemoveStatus Remove { get; set; }
public abstract LiberateButtonStatus Liberate { get; }
public Image Cover
{
get => _cover;
protected set
{
_cover = value;
NotifyPropertyChanged();
}
}
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 virtual LastDownloadStatus LastDownload { get; protected set; } = new();
public string Description { get; protected set; }
public string ProductRating { get; protected set; }
protected Rating _myRating;
public Rating MyRating
{
get => _myRating;
set
{
if (_myRating != value
&& value.OverallRating != 0
&& updateReviewTask?.IsCompleted is not false)
{
updateReviewTask = UpdateRating(value);
updateReviewTask.ContinueWith(t =>
{
if (t.Result)
{
_myRating = value;
LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, value);
}
NotifyPropertyChanged();
});
}
}
}
public abstract string DisplayTags { get; }
#endregion
#region User rating
private Task<bool> updateReviewTask;
private async Task<bool> UpdateRating(Rating rating)
{
var api = await LibraryBook.GetApiAsync();
return await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating);
}
#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<string, Func<object>> CreateMemberValueDictionary();
private Dictionary<string, Func<object>> _memberValues { get; set; }
// Instantiate comparers for every exposed member object type.
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
{
{ typeof(RemoveStatus), new ObjectComparer<RemoveStatus>() },
{ typeof(string), new ObjectComparer<string>() },
{ typeof(int), new ObjectComparer<int>() },
{ typeof(float), new ObjectComparer<float>() },
{ typeof(bool), new ObjectComparer<bool>() },
{ typeof(DateTime), new ObjectComparer<DateTime>() },
{ typeof(LiberateButtonStatus), new ObjectComparer<LiberateButtonStatus>() },
{ typeof(LastDownloadStatus), new ObjectComparer<LastDownloadStatus>() },
};
#endregion
#region Cover Art
private Image _cover;
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 Image loadImage(byte[] picture)
{
try
{
return ImageReader.ToImage(picture);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error loading cover art for {Book}", Book);
return Properties.Resources.default_cover_80x80;
}
}
#endregion
#region Static library display functions
/// <summary>This information should not change during <see cref="GridEntry"/> lifetime, so call only once.</summary>
protected static string GetDescriptionDisplay(Book book)
{
var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(book?.Description?.Replace("</p> ", "\r\n\r\n</p>") ?? "");
return doc.DocumentNode.InnerText.Trim();
}
protected static string TrimTextToWord(string text, int maxLength)
{
return
text.Length <= maxLength ?
text :
text.Substring(0, maxLength - 3) + "...";
}
/// <summary>
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
/// Maximum of 5 text rows will fit in 80-pixel row height.
/// </summary>
protected static string GetMiscDisplay(LibraryBook libraryBook)
{
var details = new List<string>();
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;
}
}
}

View file

@ -1,6 +1,7 @@
using ApplicationServices;
using Dinah.Core.DataBinding;
using LibationSearchEngine;
using LibationUiBase.GridView;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@ -20,20 +21,20 @@ namespace LibationWinForms.GridView
* Remove is overridden to ensure that removed items are removed from
* the base list (visible items) as well as the FilterRemoved list.
*/
internal class GridEntryBindingList : BindingList<GridEntry>, IBindingListView
internal class GridEntryBindingList : BindingList<IGridEntry>, IBindingListView
{
public GridEntryBindingList() : base(new List<GridEntry>()) { }
public GridEntryBindingList(IEnumerable<GridEntry> enumeration) : base(new List<GridEntry>(enumeration)) { }
public GridEntryBindingList() : base(new List<IGridEntry>()) { }
public GridEntryBindingList(IEnumerable<IGridEntry> enumeration) : base(new List<IGridEntry>(enumeration)) { }
/// <returns>All items in the list, including those filtered out.</returns>
public List<GridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
public List<IGridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
public bool SupportsFiltering => true;
public string Filter { get => FilterString; set => ApplyFilter(value); }
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary>
public bool SuspendFilteringOnUpdate { get; set; }
protected MemberComparer<GridEntry> Comparer { get; } = new();
protected MemberComparer<IGridEntry> Comparer { get; } = new();
protected override bool SupportsSortingCore => true;
protected override bool SupportsSearchingCore => true;
protected override bool IsSortedCore => isSorted;
@ -41,7 +42,7 @@ namespace LibationWinForms.GridView
protected override ListSortDirection SortDirectionCore => listSortDirection;
/// <summary> Items that were removed from the base list due to filtering </summary>
private readonly List<GridEntry> FilterRemoved = new();
private readonly List<IGridEntry> FilterRemoved = new();
private string FilterString;
private SearchResultSet SearchResults;
private bool isSorted;
@ -59,7 +60,7 @@ namespace LibationWinForms.GridView
public ListSortDescriptionCollection SortDescriptions => throw new NotImplementedException();
#endregion
public new void Remove(GridEntry entry)
public new void Remove(IGridEntry entry)
{
FilterRemoved.Remove(entry);
base.Remove(entry);
@ -73,7 +74,7 @@ namespace LibationWinForms.GridView
FilterString = filterString;
SearchResults = SearchEngineCommands.Search(filterString);
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
//Find all series containing children that match the search criteria
var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
@ -99,7 +100,7 @@ namespace LibationWinForms.GridView
ExpandItem(series);
}
public void CollapseItem(SeriesEntry sEntry)
public void CollapseItem(ISeriesEntry sEntry)
{
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).ToList())
{
@ -110,7 +111,7 @@ namespace LibationWinForms.GridView
sEntry.Liberate.Expanded = false;
}
public void ExpandItem(SeriesEntry sEntry)
public void ExpandItem(ISeriesEntry sEntry)
{
var sindex = Items.IndexOf(sEntry);
@ -133,7 +134,7 @@ namespace LibationWinForms.GridView
foreach (var item in FilterRemoved.ToList())
{
if (item is SeriesEntry || (item is LibraryBookEntry lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded)))
if (item is ISeriesEntry || (item is ILibraryBookEntry lbe && (lbe.Liberate.IsBook || lbe.Parent.Liberate.Expanded)))
{
FilterRemoved.Remove(item);
InsertItem(visibleCount++, item);
@ -145,7 +146,7 @@ namespace LibationWinForms.GridView
else
//No user sort is applied, so do default sorting by DateAdded, descending
{
Comparer.PropertyName = nameof(GridEntry.DateAdded);
Comparer.PropertyName = nameof(IGridEntry.DateAdded);
Comparer.Direction = ListSortDirection.Descending;
Sort();
}
@ -172,9 +173,9 @@ namespace LibationWinForms.GridView
protected void Sort()
{
var itemsList = (List<GridEntry>)Items;
var itemsList = (List<IGridEntry>)Items;
var children = itemsList.BookEntries().Where(i => i.Parent is not null).ToList();
var children = itemsList.BookEntries().Where(i => i.Liberate.IsEpisode).ToList();
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
@ -198,7 +199,7 @@ namespace LibationWinForms.GridView
{
if (e.ListChangedType == ListChangedType.ItemChanged)
{
if (FilterString is not null && !SuspendFilteringOnUpdate && Items[e.NewIndex] is LibraryBookEntry lbItem)
if (FilterString is not null && !SuspendFilteringOnUpdate && Items[e.NewIndex] is ILibraryBookEntry lbItem)
{
SearchResults = SearchEngineCommands.Search(FilterString);
if (!SearchResults.Docs.Any(d => d.ProductId == lbItem.AudibleProductId))

View file

@ -1,4 +1,4 @@
using LibationUiBase;
using LibationUiBase.GridView;
using System;
using System.Drawing;
using System.Windows.Forms;
@ -26,7 +26,8 @@ namespace LibationWinForms.GridView
private LastDownloadStatus LastDownload => (LastDownloadStatus)Value;
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
ToolTipText = ((LastDownloadStatus)value).ToolTipText;
if (value is LastDownloadStatus lastDl)
ToolTipText = lastDl.ToolTipText;
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);
}

View file

@ -1,38 +0,0 @@
using DataLayer;
using System;
namespace LibationWinForms.GridView
{
public class LiberateButtonStatus : IComparable
{
public LiberatedStatus BookStatus { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
public bool Expanded { get; set; }
public bool IsSeries { get; }
private bool IsAbsent { get; }
public bool IsUnavailable => !IsSeries & IsAbsent & (BookStatus is not LiberatedStatus.Liberated || PdfStatus is not null and not LiberatedStatus.Liberated);
public LiberateButtonStatus(bool isSeries, bool isAbsent)
{
IsSeries = isSeries;
IsAbsent = isAbsent;
}
/// <summary>
/// Defines the Liberate column's sorting behavior
/// </summary>
public int CompareTo(object obj)
{
if (obj is not LiberateButtonStatus second) return -1;
if (IsSeries && !second.IsSeries) return -1;
else if (!IsSeries && second.IsSeries) return 1;
else if (IsSeries && second.IsSeries) return 0;
else if (IsUnavailable && !second.IsUnavailable) return 1;
else if (!IsUnavailable && second.IsUnavailable) return -1;
else if (BookStatus == LiberatedStatus.Liberated && second.BookStatus != LiberatedStatus.Liberated) return -1;
else if (BookStatus != LiberatedStatus.Liberated && second.BookStatus == LiberatedStatus.Liberated) return 1;
else return BookStatus.CompareTo(second.BookStatus);
}
}
}

View file

@ -1,8 +1,6 @@
using System;
using DataLayer;
using System.Drawing;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core.WindowsDesktop.Forms;
namespace LibationWinForms.GridView
{
@ -16,78 +14,26 @@ namespace LibationWinForms.GridView
internal class LiberateDataGridViewImageButtonCell : DataGridViewImageButtonCell
{
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
private static readonly Brush DISABLED_GRAY = new SolidBrush(Color.FromArgb(0x60, Color.LightGray));
private static readonly Color HiddenForeColor = Color.LightGray;
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
if (value is LiberateButtonStatus status)
if (value is WinFormsEntryStatus status)
{
if (status.BookStatus is LiberatedStatus.Error || status.IsUnavailable)
//Don't paint the button graphic
paintParts ^= DataGridViewPaintParts.ContentBackground | DataGridViewPaintParts.ContentForeground | DataGridViewPaintParts.SelectionBackground;
if (rowIndex >= 0 && DataGridView.GetBoundItem<GridEntry>(rowIndex) is LibraryBookEntry lbEntry && lbEntry.Parent is not null)
DataGridView.Rows[rowIndex].DefaultCellStyle.BackColor = SERIES_BG_COLOR;
DataGridView.Rows[rowIndex].DefaultCellStyle.BackColor = (Color)status.BackgroundBrush;
DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor = status.Opacity == 1 ? DataGridView.DefaultCellStyle.ForeColor : HiddenForeColor;
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
if (status.IsSeries)
{
DrawButtonImage(graphics, status.Expanded ? Properties.Resources.minus : Properties.Resources.plus, cellBounds);
DrawButtonImage(graphics, (Image)status.ButtonImage, cellBounds);
ToolTipText = status.ToolTip;
ToolTipText = status.Expanded ? "Click to Collpase" : "Click to Expand";
}
else
{
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(status.BookStatus, status.PdfStatus);
DrawButtonImage(graphics, buttonImage, cellBounds);
if (status.IsUnavailable)
{
//Create the "disabled" look by painting a transparent gray box over the buttom image.
graphics.FillRectangle(DISABLED_GRAY, cellBounds);
ToolTipText = "This book cannot be downloaded\r\nbecause it wasn't found during\r\nthe most recent library scan";
}
else
ToolTipText = mouseoverText;
}
if (status.IsUnavailable || status.Opacity < 1)
graphics.FillRectangle(DISABLED_GRAY, cellBounds);
}
}
private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedStatus liberatedStatus, LiberatedStatus? pdfStatus)
{
if (liberatedStatus == LiberatedStatus.Error)
return ("Book downloaded ERROR", Properties.Resources.error);
(string libState, string image_lib) = liberatedStatus switch
{
LiberatedStatus.Liberated => ("Liberated", "green"),
LiberatedStatus.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
LiberatedStatus.NotLiberated => ("Book NOT downloaded", "red"),
_ => throw new Exception("Unexpected liberation state")
};
(string pdfState, string image_pdf) = pdfStatus switch
{
LiberatedStatus.Liberated => ("\r\nPDF downloaded", "_pdf_yes"),
LiberatedStatus.NotLiberated => ("\r\nPDF NOT downloaded", "_pdf_no"),
LiberatedStatus.Error => ("\r\nPDF downloaded ERROR", "_pdf_no"),
null => ("", ""),
_ => throw new Exception("Unexpected PDF state")
};
var mouseoverText = libState + pdfState;
if (liberatedStatus == LiberatedStatus.NotLiberated ||
liberatedStatus == LiberatedStatus.PartialDownload ||
pdfStatus == LiberatedStatus.NotLiberated)
mouseoverText += "\r\nClick to complete";
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
return (mouseoverText, buttonImage);
}
}
}

View file

@ -1,177 +0,0 @@
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using LibationUiBase;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms.GridView
{
/// <summary>The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode</summary>
public class LibraryBookEntry : GridEntry
{
[Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded;
[Browsable(false)] public SeriesEntry Parent { get; init; }
[Browsable(false)] public override bool IsSeries => false;
[Browsable(false)] public override bool IsEpisode => Parent is not null;
[Browsable(false)] public override bool IsBook => Parent is null;
#region Model properties exposed to the view
private DateTime lastStatusUpdate = default;
private LiberatedStatus _bookStatus;
private LiberatedStatus? _pdfStatus;
public override LastDownloadStatus LastDownload { get; protected set; }
public override RemoveStatus Remove
{
get
{
return _remove;
}
set
{
_remove = value is RemoveStatus.SomeRemoved ? RemoveStatus.NotRemoved : value;
Parent?.ChildRemoveUpdate();
NotifyPropertyChanged();
}
}
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 string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
#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);
NotifyPropertyChanged();
}
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";
_myRating = Book.UserDefinedItem.Rating;
PurchaseDate = libraryBook.DateAdded.ToString("d");
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
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;
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
#region detect changes to the model, update the view, and save to database.
/// <summary>
/// This event handler receives notifications from the model that it has changed.
/// Notify the view that it's changed.
/// </summary>
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;
NotifyPropertyChanged(nameof(DisplayTags));
break;
case nameof(udi.BookStatus):
Book.UserDefinedItem.BookStatus = udi.BookStatus;
_bookStatus = udi.BookStatus;
NotifyPropertyChanged(nameof(Liberate));
break;
case nameof(udi.PdfStatus):
Book.UserDefinedItem.SetPdfStatus(udi.PdfStatus);
_pdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate));
break;
case nameof(udi.LastDownloaded):
LastDownload = new(udi);
NotifyPropertyChanged(nameof(LastDownload));
break;
}
}
/// <summary>Save edits to the database</summary>
public void Commit(string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
// MVVM pass-through
=> Book.UpdateUserDefinedItem(newTags, bookStatus: bookStatus, pdfStatus: pdfStatus);
#endregion
#region Data Sorting
/// <summary>Create getters for all member object values by name </summary>
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
{
{ nameof(Remove), () => Remove },
{ 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(DisplayTags), () => DisplayTags },
{ nameof(Liberate), () => Liberate },
{ nameof(DateAdded), () => DateAdded },
};
#endregion
~LibraryBookEntry()
{
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
}
}
}

View file

@ -31,7 +31,7 @@ namespace LibationWinForms.GridView
public override Type EditType => typeof(MyRatingCellEditor);
public override Type ValueType => typeof(Rating);
public MyRatingGridViewCell() { ToolTipText = "Click to change ratings"; }
public MyRatingGridViewCell() { ToolTipText = ReadOnly ? "" : "Click to change ratings"; }
public override void InitializeEditingControl(int rowIndex, object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
{
@ -46,7 +46,7 @@ namespace LibationWinForms.GridView
{
if (value is Rating rating)
{
ToolTipText = "Click to change ratings";
ToolTipText = ReadOnly ? "" : "Click to change ratings";
var starString = rating.ToStarString();
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, starString, starString, errorText, cellStyle, advancedBorderStyle, paintParts);

View file

@ -3,6 +3,7 @@ using AudibleUtilities;
using DataLayer;
using FileLiberator;
using LibationFileManager;
using LibationUiBase.GridView;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
@ -32,7 +33,7 @@ namespace LibationWinForms.GridView
#region Button controls
private ImageDisplay imageDisplay;
private void productsGrid_CoverClicked(GridEntry liveGridEntry)
private void productsGrid_CoverClicked(IGridEntry liveGridEntry)
{
var picDef = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
@ -67,7 +68,7 @@ namespace LibationWinForms.GridView
imageDisplay.Show(null);
}
private void productsGrid_DescriptionClicked(GridEntry liveGridEntry, Rectangle cellRectangle)
private void productsGrid_DescriptionClicked(IGridEntry liveGridEntry, Rectangle cellRectangle)
{
var displayWindow = new DescriptionDisplay
{
@ -86,11 +87,11 @@ namespace LibationWinForms.GridView
displayWindow.Show(this);
}
private void productsGrid_DetailsClicked(LibraryBookEntry liveGridEntry)
private void productsGrid_DetailsClicked(ILibraryBookEntry liveGridEntry)
{
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
if (bookDetailsForm.ShowDialog() == DialogResult.OK)
liveGridEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
liveGridEntry.Book.UpdateUserDefinedItem(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
}
#endregion
@ -102,7 +103,7 @@ namespace LibationWinForms.GridView
public async Task RemoveCheckedBooksAsync()
{
var selectedBooks = productsGrid.GetAllBookEntries().Where(lbe => lbe.Remove is RemoveStatus.Removed).ToList();
var selectedBooks = productsGrid.GetAllBookEntries().Where(lbe => lbe.Remove is true).ToList();
if (selectedBooks.Count == 0)
return;
@ -110,8 +111,8 @@ namespace LibationWinForms.GridView
var booksToRemove = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = MessageBoxLib.ShowConfirmationDialog(
booksToRemove,
// do not use `$` string interpolation. See impl.
"Are you sure you want to remove {0} from Libation's library?",
// do not use `$` string interpolation. See impl.
"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?");
if (result != DialogResult.Yes)
@ -141,7 +142,7 @@ namespace LibationWinForms.GridView
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
foreach (var r in removable)
r.Remove = RemoveStatus.Removed;
r.Remove = true;
productsGrid_RemovableCountChanged(this, null);
}
@ -198,14 +199,14 @@ namespace LibationWinForms.GridView
VisibleCountChanged?.Invoke(this, count);
}
private void productsGrid_LiberateClicked(LibraryBookEntry liveGridEntry)
private void productsGrid_LiberateClicked(ILibraryBookEntry liveGridEntry)
{
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error
&& !liveGridEntry.Liberate.IsUnavailable)
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
}
private void productsGrid_ConvertToMp3Clicked(LibraryBookEntry liveGridEntry)
private void productsGrid_ConvertToMp3Clicked(ILibraryBookEntry liveGridEntry)
{
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error)
ConvertToMp3Clicked?.Invoke(this, liveGridEntry.LibraryBook);
@ -213,7 +214,7 @@ namespace LibationWinForms.GridView
private void productsGrid_RemovableCountChanged(object sender, EventArgs e)
{
RemovableCountChanged?.Invoke(sender, productsGrid.GetAllBookEntries().Count(lbe => lbe.Remove is RemoveStatus.Removed));
RemovableCountChanged?.Invoke(sender, productsGrid.GetAllBookEntries().Count(lbe => lbe.Remove is true));
}
}
}

View file

@ -1,6 +1,8 @@
namespace LibationWinForms.GridView
using LibationUiBase.GridView;
namespace LibationWinForms.GridView
{
partial class ProductsGrid
partial class ProductsGrid
{
/// <summary>
/// Required designer variable.
@ -41,7 +43,7 @@
this.seriesGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.descriptionGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.categoryGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.productRatingGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.productRatingGVColumn = new LibationWinForms.GridView.MyRatingGridViewColumn();
this.purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.myRatingGVColumn = new LibationWinForms.GridView.MyRatingGridViewColumn();
this.miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
@ -193,6 +195,7 @@
this.productRatingGVColumn.HeaderText = "Product Rating";
this.productRatingGVColumn.Name = "productRatingGVColumn";
this.productRatingGVColumn.ReadOnly = true;
this.productRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.productRatingGVColumn.Width = 108;
//
// purchaseDateGVColumn
@ -229,7 +232,7 @@
//
// tagAndDetailsGVColumn
//
this.tagAndDetailsGVColumn.DataPropertyName = "DisplayTags";
this.tagAndDetailsGVColumn.DataPropertyName = "BookTags";
this.tagAndDetailsGVColumn.HeaderText = "Tags and Details";
this.tagAndDetailsGVColumn.Name = "tagAndDetailsGVColumn";
this.tagAndDetailsGVColumn.ReadOnly = true;
@ -243,7 +246,7 @@
//
// syncBindingSource
//
this.syncBindingSource.DataSource = typeof(LibationWinForms.GridView.GridEntry);
this.syncBindingSource.DataSource = typeof(IGridEntry);
//
// ProductsGrid
//
@ -275,7 +278,7 @@
private System.Windows.Forms.DataGridViewTextBoxColumn seriesGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn descriptionGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn categoryGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn productRatingGVColumn;
private MyRatingGridViewColumn productRatingGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn purchaseDateGVColumn;
private MyRatingGridViewColumn myRatingGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;

View file

@ -1,21 +1,23 @@
using System;
using ApplicationServices;
using DataLayer;
using Dinah.Core.WindowsDesktop.Forms;
using LibationFileManager;
using LibationUiBase.GridView;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using DataLayer;
using Dinah.Core.WindowsDesktop.Forms;
using LibationFileManager;
using LibationWinForms.Dialogs;
namespace LibationWinForms.GridView
{
public delegate void GridEntryClickedEventHandler(GridEntry liveGridEntry);
public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry);
public delegate void GridEntryRectangleClickedEventHandler(GridEntry liveGridEntry, Rectangle cellRectangle);
public delegate void GridEntryClickedEventHandler(IGridEntry liveGridEntry);
public delegate void LibraryBookEntryClickedEventHandler(ILibraryBookEntry liveGridEntry);
public delegate void GridEntryRectangleClickedEventHandler(IGridEntry liveGridEntry, Rectangle cellRectangle);
public partial class ProductsGrid : UserControl
{
@ -34,7 +36,7 @@ namespace LibationWinForms.GridView
=> bindingList
.BookEntries()
.Select(lbe => lbe.LibraryBook);
internal IEnumerable<LibraryBookEntry> GetAllBookEntries()
internal IEnumerable<ILibraryBookEntry> GetAllBookEntries()
=> bindingList.AllItems().BookEntries();
public ProductsGrid()
@ -62,7 +64,7 @@ namespace LibationWinForms.GridView
return;
var entry = getGridEntry(e.RowIndex);
if (entry is LibraryBookEntry lbEntry)
if (entry is ILibraryBookEntry lbEntry)
{
if (e.ColumnIndex == liberateGVColumn.Index)
LiberateClicked?.Invoke(lbEntry);
@ -73,7 +75,7 @@ namespace LibationWinForms.GridView
else if (e.ColumnIndex == coverGVColumn.Index)
CoverClicked?.Invoke(lbEntry);
}
else if (entry is SeriesEntry sEntry)
else if (entry is ISeriesEntry sEntry)
{
if (e.ColumnIndex == liberateGVColumn.Index)
{
@ -82,8 +84,6 @@ namespace LibationWinForms.GridView
else
bindingList.ExpandItem(sEntry);
sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate));
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
else if (e.ColumnIndex == descriptionGVColumn.Index)
@ -98,108 +98,108 @@ namespace LibationWinForms.GridView
RemovableCountChanged?.Invoke(this, EventArgs.Empty);
}
}
catch(Exception ex)
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"An error was encountered while processing a user click in the {nameof(ProductsGrid)}");
}
}
private void gridEntryDataGridView_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
{
private void gridEntryDataGridView_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
{
// header
if (e.RowIndex < 0)
return;
if (e.RowIndex < 0)
return;
// cover
if (e.ColumnIndex == coverGVColumn.Index)
return;
// cover
if (e.ColumnIndex == coverGVColumn.Index)
return;
// any non-stop light
if (e.ColumnIndex != liberateGVColumn.Index)
// any non-stop light
if (e.ColumnIndex != liberateGVColumn.Index)
{
var copyContextMenu = new ContextMenuStrip();
copyContextMenu.Items.Add("Copy", null, (_, __) =>
var copyContextMenu = new ContextMenuStrip();
copyContextMenu.Items.Add("Copy", null, (_, __) =>
{
try
{
var dgv = (DataGridView)sender;
var text = dgv[e.ColumnIndex, e.RowIndex].FormattedValue.ToString();
{
var dgv = (DataGridView)sender;
var text = dgv[e.ColumnIndex, e.RowIndex].FormattedValue.ToString();
Clipboard.SetDataObject(text, false, 5, 150);
}
}
catch { }
});
});
e.ContextMenuStrip = copyContextMenu;
return;
}
e.ContextMenuStrip = copyContextMenu;
return;
}
// else: stop light
var entry = getGridEntry(e.RowIndex);
if (entry.IsSeries)
var entry = getGridEntry(e.RowIndex);
if (entry.Liberate.IsSeries)
return;
var setDownloadMenuItem = new ToolStripMenuItem()
{
Text = "Set Download status to '&Downloaded'",
Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated
{
Text = "Set Download status to '&Downloaded'",
Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated
};
setDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.Liberated);
setDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.Liberated);
var setNotDownloadMenuItem = new ToolStripMenuItem()
{
Text = "Set Download status to '&Not Downloaded'",
Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated
};
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
var setNotDownloadMenuItem = new ToolStripMenuItem()
{
Text = "Set Download status to '&Not Downloaded'",
Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated
};
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
var removeMenuItem = new ToolStripMenuItem() { Text = "&Remove from library" };
var removeMenuItem = new ToolStripMenuItem() { Text = "&Remove from library" };
removeMenuItem.Click += async (_, __) => await Task.Run(entry.LibraryBook.RemoveBook);
var locateFileMenuItem = new ToolStripMenuItem() { Text = "&Locate file..." };
locateFileMenuItem.Click += (_, __) =>
{
try
{
var openFileDialog = new OpenFileDialog
{
Title = $"Locate the audio file for '{entry.Book.Title}'",
Filter = "All files (*.*)|*.*",
FilterIndex = 1
};
if (openFileDialog.ShowDialog() == DialogResult.OK)
FilePathCache.Insert(entry.AudibleProductId, openFileDialog.FileName);
}
catch (Exception ex)
{
var msg = "Error saving book's location";
MessageBoxLib.ShowAdminAlert(this, msg, msg, ex);
}
};
var locateFileMenuItem = new ToolStripMenuItem() { Text = "&Locate file..." };
locateFileMenuItem.Click += (_, __) =>
{
try
{
var openFileDialog = new OpenFileDialog
{
Title = $"Locate the audio file for '{entry.Book.Title}'",
Filter = "All files (*.*)|*.*",
FilterIndex = 1
};
if (openFileDialog.ShowDialog() == DialogResult.OK)
FilePathCache.Insert(entry.AudibleProductId, openFileDialog.FileName);
}
catch (Exception ex)
{
var msg = "Error saving book's location";
MessageBoxLib.ShowAdminAlert(this, msg, msg, ex);
}
};
var convertToMp3MenuItem = new ToolStripMenuItem
{
Text = "&Convert to Mp3",
Enabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
};
convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(entry as LibraryBookEntry);
convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(entry as ILibraryBookEntry);
var bookRecordMenuItem = new ToolStripMenuItem { Text = "View &Bookmarks/Clips" };
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this);
var stopLightContextMenu = new ContextMenuStrip();
stopLightContextMenu.Items.Add(setDownloadMenuItem);
stopLightContextMenu.Items.Add(setNotDownloadMenuItem);
stopLightContextMenu.Items.Add(removeMenuItem);
stopLightContextMenu.Items.Add(locateFileMenuItem);
stopLightContextMenu.Items.Add(convertToMp3MenuItem);
stopLightContextMenu.Items.Add(setDownloadMenuItem);
stopLightContextMenu.Items.Add(setNotDownloadMenuItem);
stopLightContextMenu.Items.Add(removeMenuItem);
stopLightContextMenu.Items.Add(locateFileMenuItem);
stopLightContextMenu.Items.Add(convertToMp3MenuItem);
stopLightContextMenu.Items.Add(new ToolStripSeparator());
stopLightContextMenu.Items.Add(bookRecordMenuItem);
e.ContextMenuStrip = stopLightContextMenu;
}
}
private GridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<GridEntry>(rowIndex);
private IGridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<IGridEntry>(rowIndex);
#endregion
@ -213,7 +213,7 @@ namespace LibationWinForms.GridView
if (value)
{
foreach (var book in bindingList.AllItems())
book.Remove = RemoveStatus.NotRemoved;
book.Remove = false;
}
removeGVColumn.DisplayIndex = 0;
@ -226,9 +226,8 @@ namespace LibationWinForms.GridView
{
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
.Select(b => new LibraryBookEntry(b))
.Cast<GridEntry>()
.ToList();
.Select(b => new LibraryBookEntry<WinFormsEntryStatus>(b))
.ToList<IGridEntry>();
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
@ -240,7 +239,7 @@ namespace LibationWinForms.GridView
if (!seriesEpisodes.Any()) continue;
var seriesEntry = new SeriesEntry(parent, seriesEpisodes);
var seriesEntry = new SeriesEntry<WinFormsEntryStatus>(parent, seriesEpisodes);
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
@ -268,15 +267,25 @@ namespace LibationWinForms.GridView
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
var parentedEpisodes = dbBooks.ParentedEpisodes().ToList();
var sw = new Stopwatch();
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))
continue;
}
sw.Start();
if (parentedEpisodes.Any(lb => lb == libraryBook))
{
sw.Stop();
//Only try to add or update is this LibraryBook is a know child of a parent
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
}
sw.Stop();
}
bindingList.SuspendFilteringOnUpdate = false;
@ -297,14 +306,11 @@ namespace LibationWinForms.GridView
RemoveBooks(removedBooks);
}
public void RemoveBooks(IEnumerable<LibraryBookEntry> removedBooks)
public void RemoveBooks(IEnumerable<ILibraryBookEntry> 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);
removed.Parent.NotifyPropertyChanged();
}
foreach (var removed in removedBooks.Where(b => b.Liberate.IsEpisode))
removed.Parent.RemoveChild(removed);
//Remove series that have no children
var removedSeries =
@ -312,28 +318,28 @@ namespace LibationWinForms.GridView
.AllItems()
.EmptySeries();
foreach (var removed in removedBooks.Cast<GridEntry>().Concat(removedSeries))
foreach (var removed in removedBooks.Cast<IGridEntry>().Concat(removedSeries))
//no need to re-filter for removed books
bindingList.Remove(removed);
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
private void AddOrUpdateBook(LibraryBook book, LibraryBookEntry existingBookEntry)
private void AddOrUpdateBook(LibraryBook book, ILibraryBookEntry existingBookEntry)
{
if (existingBookEntry is null)
// Add the new product to top
bindingList.Insert(0, new LibraryBookEntry(book));
bindingList.Insert(0, new LibraryBookEntry<WinFormsEntryStatus>(book));
else
// update existing
existingBookEntry.UpdateLibraryBook(book);
}
private void AddOrUpdateEpisode(LibraryBook episodeBook, LibraryBookEntry existingEpisodeEntry, List<SeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
private void AddOrUpdateEpisode(LibraryBook episodeBook, ILibraryBookEntry existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
{
if (existingEpisodeEntry is null)
{
LibraryBookEntry episodeEntry;
ILibraryBookEntry episodeEntry;
var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
@ -351,8 +357,7 @@ namespace LibationWinForms.GridView
return;
}
seriesEntry = new SeriesEntry(seriesBook, episodeBook);
seriesEntry = new SeriesEntry<WinFormsEntryStatus>(seriesBook, episodeBook);
seriesEntries.Add(seriesEntry);
episodeEntry = seriesEntry.Children[0];
@ -362,10 +367,10 @@ namespace LibationWinForms.GridView
else
{
//Series exists. Create and add episode child then update the SeriesEntry
episodeEntry = new(episodeBook) { Parent = seriesEntry };
episodeEntry = new LibraryBookEntry<WinFormsEntryStatus>(episodeBook, seriesEntry);
seriesEntry.Children.Add(episodeEntry);
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
seriesEntry.UpdateSeries(seriesBook);
seriesEntry.UpdateLibraryBook(seriesBook);
}
//Add episode to the grid beneath the parent
@ -376,9 +381,6 @@ namespace LibationWinForms.GridView
bindingList.ExpandItem(seriesEntry);
else
bindingList.CollapseItem(seriesEntry);
seriesEntry.NotifyPropertyChanged();
}
else
existingEpisodeEntry.UpdateLibraryBook(episodeBook);
@ -399,7 +401,7 @@ namespace LibationWinForms.GridView
if (visibleCount != bindingList.Count)
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
}
#endregion
@ -465,10 +467,10 @@ namespace LibationWinForms.GridView
//Remove column is always first;
removeGVColumn.DisplayIndex = 0;
removeGVColumn.Visible = false;
removeGVColumn.ValueType = typeof(RemoveStatus);
removeGVColumn.FalseValue = RemoveStatus.NotRemoved;
removeGVColumn.TrueValue = RemoveStatus.Removed;
removeGVColumn.IndeterminateValue = RemoveStatus.SomeRemoved;
removeGVColumn.ValueType = typeof(bool?);
removeGVColumn.FalseValue = false;
removeGVColumn.TrueValue = true;
removeGVColumn.IndeterminateValue = null;
}
private void HideMenuItem_Click(object sender, EventArgs e)

View file

@ -1,44 +0,0 @@
using DataLayer;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationWinForms.GridView
{
#nullable enable
internal static class QueryExtensions
{
public static IEnumerable<LibraryBookEntry> BookEntries(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.OfType<LibraryBookEntry>();
public static IEnumerable<SeriesEntry> SeriesEntries(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.OfType<SeriesEntry>();
public static T? FindByAsin<T>(this IEnumerable<T> gridEntries, string audibleProductID) where T : GridEntry
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.SeriesEntries().Where(i => i.Children.Count == 0);
public static SeriesEntry? FindSeriesParent(this IEnumerable<GridEntry> gridEntries, LibraryBook seriesEpisode)
{
if (seriesEpisode.Book.SeriesLink is null) return null;
try
{
//Parent books will always have exactly 1 SeriesBook due to how
//they are imported in ApiExtended.getChildEpisodesAsync()
return gridEntries.SeriesEntries().FirstOrDefault(
lb =>
seriesEpisode.Book.SeriesLink.Any(
s => s.Series.AudibleSeriesId == lb.LibraryBook.Book.SeriesLink.Single().Series.AudibleSeriesId));
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Query error in {0}", nameof(FindSeriesParent));
return null;
}
}
}
#nullable disable
}

View file

@ -1,133 +0,0 @@
using DataLayer;
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms.GridView
{
/// <summary>The View Model for a LibraryBook that is ContentType.Parent</summary>
public class SeriesEntry : GridEntry
{
[Browsable(false)] public List<LibraryBookEntry> Children { get; }
[Browsable(false)] public override DateTime DateAdded => Children.Max(c => c.DateAdded);
[Browsable(false)] public override bool IsSeries => true;
[Browsable(false)] public override bool IsEpisode => false;
[Browsable(false)] public override bool IsBook => false;
private bool suspendCounting = false;
public void ChildRemoveUpdate()
{
if (suspendCounting) return;
var removeCount = Children.Count(c => c.Remove is RemoveStatus.Removed);
if (removeCount == 0)
_remove = RemoveStatus.NotRemoved;
else if (removeCount == Children.Count)
_remove = RemoveStatus.Removed;
else
_remove = RemoveStatus.SomeRemoved;
NotifyPropertyChanged(nameof(Remove));
}
#region Model properties exposed to the view
public override RemoveStatus Remove
{
get
{
return _remove;
}
set
{
_remove = value is RemoveStatus.SomeRemoved ? RemoveStatus.NotRemoved : value;
suspendCounting = true;
foreach (var item in Children)
item.Remove = value;
suspendCounting = false;
NotifyPropertyChanged();
}
}
public override LiberateButtonStatus Liberate { get; }
public override string DisplayTags { get; } = string.Empty;
#endregion
private SeriesEntry(LibraryBook parent)
{
Liberate = new LiberateButtonStatus(isSeries: true, isAbsent: false);
SeriesIndex = -1;
LibraryBook = parent;
LoadCover();
}
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children) : this(parent)
{
Children = children
.Select(c => new LibraryBookEntry(c) { Parent = this })
.OrderBy(c => c.SeriesIndex)
.ToList();
UpdateSeries(parent);
}
public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent)
{
Children = new() { new LibraryBookEntry(child) { Parent = this } };
UpdateSeries(parent);
}
public void UpdateSeries(LibraryBook parent)
{
LibraryBook = parent;
Title = Book.Title;
Series = Book.SeriesNames();
_myRating = Book.UserDefinedItem.Rating;
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
Authors = Book.AuthorNames();
Narrators = Book.NarratorNames();
Category = string.Join(" > ", Book.CategoriesNames());
Misc = GetMiscDisplay(LibraryBook);
LongDescription = GetDescriptionDisplay(Book);
Description = TrimTextToWord(LongDescription, 62);
int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
NotifyPropertyChanged();
}
#region Data Sorting
/// <summary>Create getters for all member object values by name</summary>
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
{
{ nameof(Remove), () => Remove },
{ nameof(Title), () => Book.TitleSortable() },
{ nameof(Series), () => Book.SeriesSortable() },
{ nameof(Length), () => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes) },
{ nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore() },
{ nameof(PurchaseDate), () => Children.Min(c => c.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(DisplayTags), () => string.Empty },
{ nameof(Liberate), () => Liberate },
{ nameof(DateAdded), () => DateAdded },
};
#endregion
}
}

View file

@ -0,0 +1,37 @@
using DataLayer;
using Dinah.Core.WindowsDesktop.Drawing;
using LibationUiBase.GridView;
using System;
using System.Drawing;
namespace LibationWinForms.GridView
{
public class WinFormsEntryStatus : EntryStatus, IEntryStatus
{
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
public override object BackgroundBrush => IsEpisode ? SERIES_BG_COLOR : SystemColors.ControlLightLight;
private WinFormsEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
public static EntryStatus Create(LibraryBook libraryBook) => new WinFormsEntryStatus(libraryBook);
protected override object LoadImage(byte[] picture)
{
try
{
return ImageReader.ToImage(picture);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error loading cover art for {Book}", Book);
return Properties.Resources.default_cover_80x80;
}
}
protected override Image GetResourceImage(string rescName)
{
var image = Properties.Resources.ResourceManager.GetObject(rescName);
return image as Bitmap;
}
}
}