WinForms and Avalonia now share all GridEntry view models
This commit is contained in:
parent
e1cd8b8f94
commit
fb9d062545
41 changed files with 1032 additions and 1704 deletions
166
Source/LibationUiBase/GridView/EntryStatus.cs
Normal file
166
Source/LibationUiBase/GridView/EntryStatus.cs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Threading;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public interface IEntryStatus
|
||||
{
|
||||
static abstract EntryStatus Create(LibraryBook libraryBook);
|
||||
}
|
||||
|
||||
//This Class holds all book entry status info to help the grid properly render entries.
|
||||
//The reason this info is in here instead of GridEntry is because all of this info is needed
|
||||
//for the "Liberate" column's display and sorting functions.
|
||||
public abstract class EntryStatus : SynchronizeInvoker, IComparable, INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public LiberatedStatus? PdfStatus => LibraryCommands.Pdf_Status(Book);
|
||||
public LiberatedStatus BookStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsSeries) return default;
|
||||
|
||||
if ((DateTime.Now - lastBookUpdate).TotalSeconds > 2)
|
||||
{
|
||||
//Cache the BookStatus so AudibleFileStorage.AaxcExists isn't
|
||||
//called multiple times per book while sorting the solumn.
|
||||
bookStatus = LibraryCommands.Liberated_Status(Book);
|
||||
lastBookUpdate = DateTime.Now;
|
||||
}
|
||||
|
||||
return bookStatus;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Expanded { get; set; }
|
||||
public bool IsSeries { get; }
|
||||
public bool IsEpisode { get; }
|
||||
public bool IsBook => !IsSeries && !IsEpisode;
|
||||
public bool IsUnavailable => !IsSeries & isAbsent & (BookStatus is not LiberatedStatus.Liberated || PdfStatus is not null and not LiberatedStatus.Liberated);
|
||||
public double Opacity => !IsSeries && Book.UserDefinedItem.Tags.ContainsInsensitive("hidden") ? 0.4 : 1;
|
||||
public abstract object BackgroundBrush { get; }
|
||||
public object ButtonImage => GetLiberateIcon();
|
||||
public string ToolTip => GetTooltip();
|
||||
protected Book Book { get; }
|
||||
|
||||
private DateTime lastBookUpdate;
|
||||
private LiberatedStatus bookStatus;
|
||||
private readonly bool isAbsent;
|
||||
private static readonly Dictionary<string, object> iconCache = new();
|
||||
|
||||
protected EntryStatus(LibraryBook libraryBook)
|
||||
{
|
||||
Book = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook)).Book;
|
||||
isAbsent = libraryBook.AbsentFromLastScan is true;
|
||||
IsEpisode = Book.ContentType is ContentType.Episode;
|
||||
IsSeries = Book.ContentType is ContentType.Parent;
|
||||
}
|
||||
|
||||
internal protected abstract object LoadImage(byte[] picture);
|
||||
protected abstract object GetResourceImage(string rescName);
|
||||
public void RaisePropertyChanged(PropertyChangedEventArgs args) => this.UIThreadSync(() => PropertyChanged?.Invoke(this, args));
|
||||
public void RaisePropertyChanged(string propertyName) => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
/// <summary>Refresh BookStatus (so partial download files are checked again in the filesystem) and raise PropertyChanged for property names.</summary>
|
||||
public void Invalidate(params string[] properties)
|
||||
{
|
||||
lastBookUpdate = default;
|
||||
foreach (var property in properties)
|
||||
RaisePropertyChanged(property);
|
||||
}
|
||||
|
||||
/// <summary> Defines the Liberate column's sorting behavior </summary>
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj is not EntryStatus 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);
|
||||
}
|
||||
|
||||
private object GetLiberateIcon()
|
||||
{
|
||||
if (IsSeries)
|
||||
return Expanded ? GetAndCacheResource("minus") : GetAndCacheResource("plus");
|
||||
|
||||
if (BookStatus == LiberatedStatus.Error)
|
||||
return GetAndCacheResource("error");
|
||||
|
||||
string image_lib = BookStatus switch
|
||||
{
|
||||
LiberatedStatus.Liberated => "green",
|
||||
LiberatedStatus.PartialDownload => "yellow",
|
||||
LiberatedStatus.NotLiberated => "red",
|
||||
_ => throw new Exception("Unexpected liberation state")
|
||||
};
|
||||
|
||||
string image_pdf = PdfStatus switch
|
||||
{
|
||||
LiberatedStatus.Liberated => "_pdf_yes",
|
||||
LiberatedStatus.NotLiberated => "_pdf_no",
|
||||
LiberatedStatus.Error => "_pdf_no",
|
||||
null => "",
|
||||
_ => throw new Exception("Unexpected PDF state")
|
||||
};
|
||||
|
||||
return GetAndCacheResource($"liberate_{image_lib}{image_pdf}");
|
||||
}
|
||||
|
||||
private string GetTooltip()
|
||||
{
|
||||
if (IsSeries)
|
||||
return Expanded ? "Click to Collpase" : "Click to Expand";
|
||||
|
||||
if (IsUnavailable)
|
||||
return "This book cannot be downloaded\nbecause it wasn't found during\nthe most recent library scan";
|
||||
|
||||
if (BookStatus == LiberatedStatus.Error)
|
||||
return "Book downloaded ERROR";
|
||||
|
||||
string libState = BookStatus switch
|
||||
{
|
||||
LiberatedStatus.Liberated => "Liberated",
|
||||
LiberatedStatus.PartialDownload => "File has been at least\r\npartially downloaded",
|
||||
LiberatedStatus.NotLiberated => "Book NOT downloaded",
|
||||
_ => throw new Exception("Unexpected liberation state")
|
||||
};
|
||||
|
||||
string pdfState = PdfStatus switch
|
||||
{
|
||||
LiberatedStatus.Liberated => "\r\nPDF downloaded",
|
||||
LiberatedStatus.NotLiberated => "\r\nPDF NOT downloaded",
|
||||
LiberatedStatus.Error => "\r\nPDF downloaded ERROR",
|
||||
null => "",
|
||||
_ => throw new Exception("Unexpected PDF state")
|
||||
};
|
||||
|
||||
var mouseoverText = libState + pdfState;
|
||||
|
||||
if (BookStatus == LiberatedStatus.NotLiberated ||
|
||||
BookStatus == LiberatedStatus.PartialDownload ||
|
||||
PdfStatus == LiberatedStatus.NotLiberated)
|
||||
mouseoverText += "\r\nClick to complete";
|
||||
|
||||
return mouseoverText;
|
||||
}
|
||||
|
||||
private object GetAndCacheResource(string rescName)
|
||||
{
|
||||
if (!iconCache.ContainsKey(rescName))
|
||||
iconCache[rescName] = GetResourceImage(rescName);
|
||||
return iconCache[rescName];
|
||||
}
|
||||
}
|
||||
}
|
||||
324
Source/LibationUiBase/GridView/GridEntry[TStatus].cs
Normal file
324
Source/LibationUiBase/GridView/GridEntry[TStatus].cs
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Threading;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public enum RemoveStatus
|
||||
{
|
||||
NotRemoved,
|
||||
Removed,
|
||||
SomeRemoved
|
||||
}
|
||||
|
||||
/// <summary>The View Model base for the DataGridView</summary>
|
||||
public abstract class GridEntry<TStatus> : SynchronizeInvoker, IGridEntry where TStatus : IEntryStatus
|
||||
{
|
||||
[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;
|
||||
|
||||
#region Model properties exposed to the view
|
||||
|
||||
protected bool? remove = false;
|
||||
private string _purchasedate;
|
||||
private string _length;
|
||||
private LastDownloadStatus _lastDownload;
|
||||
private object _cover;
|
||||
private string _series;
|
||||
private string _title;
|
||||
private string _authors;
|
||||
private string _narrators;
|
||||
private string _category;
|
||||
private string _misc;
|
||||
private string _description;
|
||||
private Rating _productrating;
|
||||
private string _bookTags;
|
||||
private Rating _myRating;
|
||||
|
||||
public abstract bool? Remove { get; set; }
|
||||
public EntryStatus Liberate { get; private set; }
|
||||
public string PurchaseDate { get => _purchasedate; protected set => RaiseAndSetIfChanged(ref _purchasedate, value); }
|
||||
public string Length { get => _length; protected set => RaiseAndSetIfChanged(ref _length, value); }
|
||||
public LastDownloadStatus LastDownload { get => _lastDownload; protected set => RaiseAndSetIfChanged(ref _lastDownload, value); }
|
||||
public object Cover { get => _cover; private set => RaiseAndSetIfChanged(ref _cover, value); }
|
||||
public string Series { get => _series; private set => RaiseAndSetIfChanged(ref _series, value); }
|
||||
public string Title { get => _title; private set => RaiseAndSetIfChanged(ref _title, value); }
|
||||
public string Authors { get => _authors; private set => RaiseAndSetIfChanged(ref _authors, value); }
|
||||
public string Narrators { get => _narrators; private set => RaiseAndSetIfChanged(ref _narrators, value); }
|
||||
public string Category { get => _category; private set => RaiseAndSetIfChanged(ref _category, value); }
|
||||
public string Misc { get => _misc; private set => RaiseAndSetIfChanged(ref _misc, value); }
|
||||
public string Description { get => _description; private set => RaiseAndSetIfChanged(ref _description, value); }
|
||||
public Rating ProductRating { get => _productrating; private set => RaiseAndSetIfChanged(ref _productrating, value); }
|
||||
public string BookTags { get => _bookTags; private set => RaiseAndSetIfChanged(ref _bookTags, value); }
|
||||
|
||||
public Rating MyRating
|
||||
{
|
||||
get => _myRating;
|
||||
set
|
||||
{
|
||||
if (_myRating != value && value.OverallRating != 0 && updateReviewTask?.IsCompleted is not false)
|
||||
updateReviewTask = UpdateRating(value);
|
||||
}
|
||||
}
|
||||
|
||||
#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))
|
||||
LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region View property updating
|
||||
|
||||
public void UpdateLibraryBook(LibraryBook libraryBook)
|
||||
{
|
||||
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
|
||||
|
||||
LibraryBook = libraryBook;
|
||||
|
||||
Liberate = TStatus.Create(libraryBook);
|
||||
Title = Book.Title;
|
||||
Series = Book.SeriesNames();
|
||||
Length = GetBookLengthString();
|
||||
//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 = GetPurchaseDateString();
|
||||
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;
|
||||
BookTags = GetBookTags();
|
||||
|
||||
RaisePropertyChanged(nameof(MyRating));
|
||||
|
||||
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
|
||||
}
|
||||
|
||||
protected abstract string GetBookTags();
|
||||
protected virtual DateTime GetPurchaseDate() => LibraryBook.DateAdded;
|
||||
protected virtual int GetLengthInMinutes() => Book.LengthInMinutes;
|
||||
protected string GetPurchaseDateString() => GetPurchaseDate().ToString("d");
|
||||
protected string GetBookLengthString()
|
||||
{
|
||||
int bookLenMins = GetLengthInMinutes();
|
||||
return bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region detect changes to the model, update the view.
|
||||
|
||||
/// <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.BookStatus):
|
||||
case nameof(udi.PdfStatus):
|
||||
Liberate.Invalidate(nameof(Liberate.BookStatus), nameof(Liberate.PdfStatus), nameof(Liberate.IsUnavailable), nameof(Liberate.ButtonImage), nameof(Liberate.ToolTip));
|
||||
RaisePropertyChanged(nameof(Liberate));
|
||||
break;
|
||||
case nameof(udi.Tags):
|
||||
BookTags = GetBookTags();
|
||||
Liberate.Invalidate(nameof(Liberate.Opacity));
|
||||
RaisePropertyChanged(nameof(Liberate));
|
||||
break;
|
||||
case nameof(udi.LastDownloaded):
|
||||
LastDownload = new (udi);
|
||||
break;
|
||||
case nameof(udi.Rating):
|
||||
_myRating = udi.Rating;
|
||||
RaisePropertyChanged(nameof(MyRating));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private TRet RaiseAndSetIfChanged<TRet>(ref TRet backingField, TRet newValue, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<TRet>.Default.Equals(backingField, newValue)) return newValue;
|
||||
|
||||
backingField = newValue;
|
||||
RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
|
||||
return newValue;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public void RaisePropertyChanged(PropertyChangedEventArgs args) => this.UIThreadSync(() => PropertyChanged?.Invoke(this, args));
|
||||
public void RaisePropertyChanged(string propertyName) => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sorting
|
||||
|
||||
public GridEntry()
|
||||
{
|
||||
memberValues = new()
|
||||
{
|
||||
{ nameof(Remove), () => Remove.HasValue ? Remove.Value ? RemoveStatus.Removed : RemoveStatus.NotRemoved : RemoveStatus.SomeRemoved },
|
||||
{ nameof(Title), () => Book.TitleSortable() },
|
||||
{ nameof(Series), () => Book.SeriesSortable() },
|
||||
{ nameof(Length), () => GetLengthInMinutes() },
|
||||
{ nameof(MyRating), () => Book.UserDefinedItem.Rating },
|
||||
{ nameof(PurchaseDate), () => GetPurchaseDate() },
|
||||
{ nameof(ProductRating), () => Book.Rating },
|
||||
{ nameof(Authors), () => Authors },
|
||||
{ nameof(Narrators), () => Narrators },
|
||||
{ nameof(Description), () => Description },
|
||||
{ nameof(Category), () => Category },
|
||||
{ nameof(Misc), () => Misc },
|
||||
{ nameof(LastDownload), () => LastDownload },
|
||||
{ nameof(BookTags), () => BookTags ?? string.Empty },
|
||||
{ nameof(Liberate), () => Liberate },
|
||||
{ nameof(DateAdded), () => DateAdded },
|
||||
};
|
||||
}
|
||||
|
||||
public object GetMemberValue(string memberName) => memberValues[memberName]();
|
||||
public IComparer GetMemberComparer(Type memberType)
|
||||
=> memberTypeComparers.TryGetValue(memberType, out IComparer value) ? value : memberTypeComparers[memberType.BaseType];
|
||||
|
||||
private readonly Dictionary<string, Func<object>> memberValues;
|
||||
|
||||
// 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(Rating), new ObjectComparer<Rating>() },
|
||||
{ typeof(DateTime), new ObjectComparer<DateTime>() },
|
||||
{ typeof(EntryStatus), new ObjectComparer<EntryStatus>() },
|
||||
{ typeof(LastDownloadStatus), new ObjectComparer<LastDownloadStatus>() },
|
||||
};
|
||||
|
||||
#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 = Liberate.LoadImage(picture);
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||
{
|
||||
// state validation
|
||||
if (e?.Definition.PictureId is null ||
|
||||
Book?.PictureId is null ||
|
||||
e.Picture?.Length == 0)
|
||||
return;
|
||||
|
||||
// logic validation
|
||||
if (e.Definition.PictureId == Book.PictureId)
|
||||
{
|
||||
Cover = Liberate.LoadImage(e.Picture);
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static library display functions
|
||||
|
||||
/// <summary>This information should not change during <see cref="GridEntry"/> lifetime, so call only once.</summary>
|
||||
private 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();
|
||||
}
|
||||
|
||||
private 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>
|
||||
private 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;
|
||||
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Source/LibationUiBase/GridView/IGridEntry.cs
Normal file
34
Source/LibationUiBase/GridView/IGridEntry.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public interface IGridEntry : IMemberComparable, INotifyPropertyChanged
|
||||
{
|
||||
EntryStatus Liberate { get; }
|
||||
float SeriesIndex { get; }
|
||||
string AudibleProductId { get; }
|
||||
string LongDescription { get; }
|
||||
LibraryBook LibraryBook { get; }
|
||||
Book Book { get; }
|
||||
DateTime DateAdded { get; }
|
||||
bool? Remove { get; set; }
|
||||
string PurchaseDate { get; }
|
||||
object Cover { get; }
|
||||
string Length { get; }
|
||||
LastDownloadStatus LastDownload { get; }
|
||||
string Series { get; }
|
||||
string Title { get; }
|
||||
string Authors { get; }
|
||||
string Narrators { get; }
|
||||
string Category { get; }
|
||||
string Misc { get; }
|
||||
string Description { get; }
|
||||
Rating ProductRating { get; }
|
||||
Rating MyRating { get; set; }
|
||||
string BookTags { get; }
|
||||
void UpdateLibraryBook(LibraryBook libraryBook);
|
||||
}
|
||||
}
|
||||
7
Source/LibationUiBase/GridView/ILibraryBookEntry.cs
Normal file
7
Source/LibationUiBase/GridView/ILibraryBookEntry.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public interface ILibraryBookEntry : IGridEntry
|
||||
{
|
||||
ISeriesEntry Parent { get; }
|
||||
}
|
||||
}
|
||||
11
Source/LibationUiBase/GridView/ISeriesEntry.cs
Normal file
11
Source/LibationUiBase/GridView/ISeriesEntry.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public interface ISeriesEntry : IGridEntry
|
||||
{
|
||||
List<ILibraryBookEntry> Children { get; }
|
||||
void ChildRemoveUpdate();
|
||||
void RemoveChild(ILibraryBookEntry libraryBookEntry);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using DataLayer;
|
||||
using System;
|
||||
|
||||
namespace LibationUiBase
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public class LastDownloadStatus : IComparable
|
||||
{
|
||||
34
Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs
Normal file
34
Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using DataLayer;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode</summary>
|
||||
public class LibraryBookEntry<TStatus> : GridEntry<TStatus>, ILibraryBookEntry where TStatus : IEntryStatus
|
||||
{
|
||||
[Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded;
|
||||
[Browsable(false)] public ISeriesEntry Parent { get; }
|
||||
|
||||
public override bool? Remove
|
||||
{
|
||||
get => remove;
|
||||
set
|
||||
{
|
||||
remove = value ?? false;
|
||||
|
||||
Parent?.ChildRemoveUpdate();
|
||||
RaisePropertyChanged(nameof(Remove));
|
||||
}
|
||||
}
|
||||
|
||||
public LibraryBookEntry(LibraryBook libraryBook, ISeriesEntry parent = null)
|
||||
{
|
||||
Parent = parent;
|
||||
UpdateLibraryBook(libraryBook);
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
|
||||
namespace LibationUiBase
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public class ObjectComparer<T> : IComparer where T : IComparable
|
||||
{
|
||||
44
Source/LibationUiBase/GridView/QueryExtensions.cs
Normal file
44
Source/LibationUiBase/GridView/QueryExtensions.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
#nullable enable
|
||||
public static class QueryExtensions
|
||||
{
|
||||
public static IEnumerable<ILibraryBookEntry> BookEntries(this IEnumerable<IGridEntry> gridEntries)
|
||||
=> gridEntries.OfType<ILibraryBookEntry>();
|
||||
|
||||
public static IEnumerable<ISeriesEntry> SeriesEntries(this IEnumerable<IGridEntry> gridEntries)
|
||||
=> gridEntries.OfType<ISeriesEntry>();
|
||||
|
||||
public static T? FindByAsin<T>(this IEnumerable<T> gridEntries, string audibleProductID) where T : IGridEntry
|
||||
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
|
||||
|
||||
public static IEnumerable<ISeriesEntry> EmptySeries(this IEnumerable<IGridEntry> gridEntries)
|
||||
=> gridEntries.SeriesEntries().Where(i => i.Children.Count == 0);
|
||||
|
||||
public static ISeriesEntry? FindSeriesParent(this IEnumerable<IGridEntry> 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
|
||||
}
|
||||
68
Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs
Normal file
68
Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>The View Model for a LibraryBook that is ContentType.Parent</summary>
|
||||
public class SeriesEntry<TStatus> : GridEntry<TStatus>, ISeriesEntry where TStatus : IEntryStatus
|
||||
{
|
||||
public List<ILibraryBookEntry> Children { get; }
|
||||
public override DateTime DateAdded => Children.Max(c => c.DateAdded);
|
||||
|
||||
private bool suspendCounting = false;
|
||||
public void ChildRemoveUpdate()
|
||||
{
|
||||
if (suspendCounting) return;
|
||||
|
||||
var removeCount = Children.Count(c => c.Remove == true);
|
||||
|
||||
remove = removeCount == 0 ? false : removeCount == Children.Count ? true : null;
|
||||
RaisePropertyChanged(nameof(Remove));
|
||||
}
|
||||
|
||||
public override bool? Remove
|
||||
{
|
||||
get => remove;
|
||||
set
|
||||
{
|
||||
remove = value ?? false;
|
||||
|
||||
suspendCounting = true;
|
||||
|
||||
foreach (var item in Children)
|
||||
item.Remove = value;
|
||||
|
||||
suspendCounting = false;
|
||||
RaisePropertyChanged(nameof(Remove));
|
||||
}
|
||||
}
|
||||
|
||||
public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent, new[] { child }) { }
|
||||
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children)
|
||||
{
|
||||
LastDownload = new();
|
||||
SeriesIndex = -1;
|
||||
|
||||
Children = children
|
||||
.Select(c => new LibraryBookEntry<TStatus>(c, this))
|
||||
.OrderBy(c => c.SeriesIndex)
|
||||
.ToList<ILibraryBookEntry>();
|
||||
|
||||
UpdateLibraryBook(parent);
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
public void RemoveChild(ILibraryBookEntry lbe)
|
||||
{
|
||||
Children.Remove(lbe);
|
||||
PurchaseDate = GetPurchaseDateString();
|
||||
Length = GetBookLengthString();
|
||||
}
|
||||
|
||||
protected override string GetBookTags() => null;
|
||||
protected override int GetLengthInMinutes() => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
|
||||
protected override DateTime GetPurchaseDate() => Children.Min(c => c.LibraryBook.DateAdded);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
|
||||
namespace LibationUiBase
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase
|
||||
namespace LibationUiBase
|
||||
{
|
||||
public class SampleRateSelection
|
||||
{
|
||||
|
|
|
|||
|
|
@ -121,11 +121,11 @@ namespace LibationUiBase
|
|||
|
||||
public void ClearCurrent()
|
||||
{
|
||||
lock(lockObject)
|
||||
lock (lockObject)
|
||||
Current = null;
|
||||
RebuildSecondary();
|
||||
}
|
||||
|
||||
|
||||
public bool RemoveCompleted(T item)
|
||||
{
|
||||
bool itemsRemoved;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ namespace LibationUiBase
|
|||
public event EventHandler<DownloadProgress> DownloadProgress;
|
||||
public event EventHandler<bool> DownloadCompleted;
|
||||
|
||||
public async Task CheckForUpgradeAsync(Func<UpgradeEventArgs,Task> upgradeAvailableHandler)
|
||||
public async Task CheckForUpgradeAsync(Func<UpgradeEventArgs, Task> upgradeAvailableHandler)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue