Add SeriesViewDialog
This commit is contained in:
parent
784ab73a36
commit
9ae1f0399b
27 changed files with 1293 additions and 18 deletions
13
Source/LibationUiBase/BaseUtil.cs
Normal file
13
Source/LibationUiBase/BaseUtil.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using LibationFileManager;
|
||||
using System;
|
||||
|
||||
namespace LibationUiBase
|
||||
{
|
||||
public static class BaseUtil
|
||||
{
|
||||
/// <summary>A delegate that loads image bytes into the the UI framework's image format.</summary>
|
||||
public static Func<byte[], PictureSize, object> LoadImage { get; private set; }
|
||||
public static void SetLoadImageDelegate(Func<byte[], PictureSize, object> tryLoadImage)
|
||||
=> LoadImage = tryLoadImage;
|
||||
}
|
||||
}
|
||||
133
Source/LibationUiBase/SeriesView/AyceButton.cs
Normal file
133
Source/LibationUiBase/SeriesView/AyceButton.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using ApplicationServices;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.SeriesView
|
||||
{
|
||||
internal class AyceButton : SeriesButton
|
||||
{
|
||||
//Making this event and field static prevents concurrent additions to the library.
|
||||
//Search engine indexer does not support concurrent re-indexing.
|
||||
private static event EventHandler ButtonEnabled;
|
||||
private static bool globalEnabled = true;
|
||||
|
||||
public override bool HasButtonAction => true;
|
||||
public override string DisplayText
|
||||
=> InLibrary ? "Remove\r\nFrom\r\nLibrary"
|
||||
: "FREE\r\n\r\nAdd to\r\nLibrary";
|
||||
|
||||
public override bool Enabled
|
||||
{
|
||||
get => globalEnabled;
|
||||
protected set
|
||||
{
|
||||
if (globalEnabled != value)
|
||||
{
|
||||
globalEnabled = value;
|
||||
ButtonEnabled?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal AyceButton(Item item, bool inLibrary) : base(item, inLibrary)
|
||||
{
|
||||
ButtonEnabled += DownloadButton_ButtonEnabled;
|
||||
}
|
||||
|
||||
public override async Task PerformClickAsync(LibraryBook accountBook)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
Enabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (InLibrary)
|
||||
await RemoveFromLibraryAsync(accountBook);
|
||||
else
|
||||
await AddToLibraryAsync(accountBook);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
var addRemove = InLibrary ? "remove" : "add";
|
||||
var toFrom = InLibrary ? "from" : "to";
|
||||
|
||||
Serilog.Log.Logger.Error(ex, $"Failed to {addRemove} {{book}} {toFrom} library", new { Item.ProductId, Item.TitleWithSubtitle });
|
||||
}
|
||||
finally { Enabled = true; }
|
||||
|
||||
}
|
||||
|
||||
private async Task RemoveFromLibraryAsync(LibraryBook accountBook)
|
||||
{
|
||||
Api api = await accountBook.GetApiAsync();
|
||||
|
||||
if (await api.RemoveItemFromLibraryAsync(Item.ProductId))
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var lb = context.GetLibraryBook_Flat_NoTracking(Item.ProductId);
|
||||
int result = await Task.Run((new[] { lb }).PermanentlyDeleteBooks);
|
||||
InLibrary = result == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddToLibraryAsync(LibraryBook accountBook)
|
||||
{
|
||||
Api api = await accountBook.GetApiAsync();
|
||||
|
||||
if (!await api.AddItemToLibraryAsync(Item.ProductId)) return;
|
||||
|
||||
Item item = null;
|
||||
|
||||
for (int tryCount = 0; tryCount < 5 && item is null; tryCount++)
|
||||
{
|
||||
//Wait a half second to allow the server time to update
|
||||
await Task.Delay(500);
|
||||
item = await api.GetLibraryBookAsync(Item.ProductId, LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
}
|
||||
|
||||
if (item is null) return;
|
||||
|
||||
if (item.IsEpisodes)
|
||||
{
|
||||
var seriesParent = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)
|
||||
.Select(lb => lb.Book)
|
||||
.FirstOrDefault(b => b.IsEpisodeParent() && b.AudibleProductId.In(item.Relationships.Select((Relationship r) => r.Asin)));
|
||||
|
||||
if (seriesParent is null) return;
|
||||
|
||||
item.Series = new[]
|
||||
{
|
||||
new AudibleApi.Common.Series
|
||||
{
|
||||
Asin = seriesParent.AudibleProductId,
|
||||
Sequence = item.Relationships.FirstOrDefault(r => r.Asin == seriesParent.AudibleProductId)?.Sort?.ToString() ?? "0",
|
||||
Title = seriesParent.Title
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
InLibrary = await LibraryCommands.ImportSingleToDbAsync(item, accountBook.Account, accountBook.Book.Locale) != 0;
|
||||
}
|
||||
|
||||
private void DownloadButton_ButtonEnabled(object sender, EventArgs e)
|
||||
=> OnPropertyChanged(nameof(Enabled));
|
||||
|
||||
public override int CompareTo(object ob)
|
||||
{
|
||||
if (ob is not AyceButton other) return 1;
|
||||
return other.InLibrary.CompareTo(InLibrary);
|
||||
}
|
||||
|
||||
~AyceButton()
|
||||
{
|
||||
ButtonEnabled -= DownloadButton_ButtonEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Source/LibationUiBase/SeriesView/SeriesButton.cs
Normal file
51
Source/LibationUiBase/SeriesView/SeriesButton.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Threading;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.SeriesView
|
||||
{
|
||||
/// <summary>
|
||||
/// base view model for the Series Viewer 'Availability' button column
|
||||
/// </summary>
|
||||
public abstract class SeriesButton : SynchronizeInvoker, IComparable, INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
private bool inLibrary;
|
||||
|
||||
protected Item Item { get; }
|
||||
public abstract string DisplayText { get; }
|
||||
public abstract bool HasButtonAction { get; }
|
||||
public abstract bool Enabled { get; protected set; }
|
||||
public bool InLibrary
|
||||
{
|
||||
get => inLibrary;
|
||||
protected set
|
||||
{
|
||||
if (inLibrary != value)
|
||||
{
|
||||
inLibrary = value;
|
||||
OnPropertyChanged(nameof(InLibrary));
|
||||
OnPropertyChanged(nameof(DisplayText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected SeriesButton(Item item, bool inLibrary)
|
||||
{
|
||||
Item = item;
|
||||
this.inLibrary = inLibrary;
|
||||
}
|
||||
|
||||
public abstract Task PerformClickAsync(LibraryBook accountBook);
|
||||
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
=> Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
|
||||
|
||||
public override string ToString() => DisplayText;
|
||||
|
||||
public abstract int CompareTo(object ob);
|
||||
}
|
||||
}
|
||||
151
Source/LibationUiBase/SeriesView/SeriesEntry.cs
Normal file
151
Source/LibationUiBase/SeriesView/SeriesEntry.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
using ApplicationServices;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Threading;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.SeriesView
|
||||
{
|
||||
public class SeriesItem : SynchronizeInvoker, INotifyPropertyChanged
|
||||
{
|
||||
public object Cover { get; private set; }
|
||||
public SeriesOrder Order { get; }
|
||||
public string Title => Item.TitleWithSubtitle;
|
||||
public SeriesButton Button { get; }
|
||||
public Item Item { get; }
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
private SeriesItem(Item item, string order, bool inLibrary, bool inWishList)
|
||||
{
|
||||
Item = item;
|
||||
Order = new SeriesOrder(order);
|
||||
Button = Item.Plans.Any(p => p.IsAyce) ? new AyceButton(item, inLibrary) : new WishlistButton(item, inLibrary, inWishList);
|
||||
LoadCover(item.PictureId);
|
||||
Button.PropertyChanged += DownloadButton_PropertyChanged;
|
||||
}
|
||||
|
||||
public void ViewOnAudible(string localeString)
|
||||
{
|
||||
var locale = Localization.Get(localeString);
|
||||
var link = $"https://www.audible.{locale.TopDomain}/pd/{Item.ProductId}";
|
||||
Go.To.Url(link);
|
||||
}
|
||||
|
||||
private void DownloadButton_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
=> OnPropertyChanged(nameof(Button));
|
||||
|
||||
private void OnPropertyChanged(string propertyName)
|
||||
=> Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
|
||||
|
||||
private void LoadCover(string pictureId)
|
||||
{
|
||||
var (isDefault, picture) = PictureStorage.GetPicture(new PictureDefinition(pictureId, PictureSize._80x80));
|
||||
if (isDefault)
|
||||
{
|
||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||
}
|
||||
Cover = BaseUtil.LoadImage(picture, PictureSize._80x80);
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||
{
|
||||
if (e?.Definition.PictureId != null && Item?.PictureId != null)
|
||||
{
|
||||
byte[] picture = e.Picture;
|
||||
if ((picture == null || picture.Length != 0) && e.Definition.PictureId == Item.PictureId)
|
||||
{
|
||||
Cover = BaseUtil.LoadImage(e.Picture, PictureSize._80x80);
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
OnPropertyChanged(nameof(Cover));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<Item, List<SeriesItem>>> GetAllSeriesItemsAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
|
||||
//Get Item for each series that this book belong to
|
||||
var seriesItemsTask = api.GetCatalogProductsAsync(libraryBook.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId), CatalogOptions.ResponseGroupOptions.Media | CatalogOptions.ResponseGroupOptions.Relationships);
|
||||
|
||||
using var semaphore = new SemaphoreSlim(10);
|
||||
|
||||
//Start getting the wishlist in the background
|
||||
var wishlistTask = api.GetWishListProductsAsync(
|
||||
new WishListOptions
|
||||
{
|
||||
PageNumber = 0,
|
||||
NumberOfResultPerPage = 50,
|
||||
ResponseGroups = WishListOptions.ResponseGroupOptions.None
|
||||
},
|
||||
numItemsPerRequest: 50,
|
||||
semaphore);
|
||||
|
||||
var items = new Dictionary<Item, List<Item>>();
|
||||
|
||||
//Get all children of all series
|
||||
foreach (var series in await seriesItemsTask)
|
||||
{
|
||||
//Books that are part of series have RelationshipType.Series
|
||||
//Podcast episodes have RelationshipType.Episode
|
||||
var childrenAsins = series.Relationships
|
||||
.Where(r => r.RelationshipType is RelationshipType.Series or RelationshipType.Episode && r.RelationshipToProduct is RelationshipToProduct.Child)
|
||||
.Select(r => r.Asin)
|
||||
.ToList();
|
||||
|
||||
if (childrenAsins.Count > 0)
|
||||
{
|
||||
var children = await api.GetCatalogProductsAsync(childrenAsins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS, 50, semaphore);
|
||||
|
||||
//If the price is null, this item is not available to the user
|
||||
var childrenWithPrices = children.Where(p => p.Price != null).ToList();
|
||||
|
||||
if (childrenWithPrices.Count > 0)
|
||||
items[series] = childrenWithPrices;
|
||||
}
|
||||
}
|
||||
|
||||
//Await the wishlist asins
|
||||
var wishlistAsins = (await wishlistTask).Select(w => w.Asin).ToHashSet();
|
||||
|
||||
var fullLib = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
var seriesEntries = new Dictionary<Item, List<SeriesItem>>();
|
||||
|
||||
//Create a SeriesItem liste for each series.
|
||||
foreach (var series in items.Keys)
|
||||
{
|
||||
ApiExtended.SetSeries(series, items[series]);
|
||||
|
||||
seriesEntries[series] = new List<SeriesItem>();
|
||||
|
||||
foreach (var item in items[series].Where(i => !string.IsNullOrEmpty(i.PictureId)))
|
||||
{
|
||||
var order = item.Series.Single(s => s.Asin == series.Asin).Sequence;
|
||||
//Match the account/book in the database
|
||||
var inLibrary = fullLib.Any(lb => lb.Account == libraryBook.Account && lb.Book.AudibleProductId == item.ProductId && !lb.AbsentFromLastScan);
|
||||
var inWishList = wishlistAsins.Contains(item.Asin);
|
||||
|
||||
seriesEntries[series].Add(new SeriesItem(item, order, inLibrary, inWishList));
|
||||
}
|
||||
}
|
||||
|
||||
return seriesEntries;
|
||||
}
|
||||
|
||||
~SeriesItem()
|
||||
{
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
Button.PropertyChanged -= DownloadButton_PropertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Source/LibationUiBase/SeriesView/SeriesOrder.cs
Normal file
23
Source/LibationUiBase/SeriesView/SeriesOrder.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
|
||||
namespace LibationUiBase.SeriesView
|
||||
{
|
||||
public class SeriesOrder : IComparable
|
||||
{
|
||||
public float Order { get; }
|
||||
public string OrderString { get; }
|
||||
|
||||
public SeriesOrder(string orderString)
|
||||
{
|
||||
OrderString = orderString;
|
||||
Order = float.TryParse(orderString, out var o) ? o : -1f;
|
||||
}
|
||||
public override string ToString() => OrderString;
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj is not SeriesOrder other) return 1;
|
||||
return Order.CompareTo(other.Order);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Source/LibationUiBase/SeriesView/WishlistButton.cs
Normal file
93
Source/LibationUiBase/SeriesView/WishlistButton.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using AudibleApi;
|
||||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.SeriesView
|
||||
{
|
||||
internal class WishlistButton : SeriesButton
|
||||
{
|
||||
private bool instanceEnabled = true;
|
||||
|
||||
private bool inWishList;
|
||||
|
||||
public override bool HasButtonAction => !InLibrary;
|
||||
public override string DisplayText
|
||||
=> InLibrary ? "Already\r\nOwned"
|
||||
: InWishList ? "Remove\r\nFrom\r\nWishlist"
|
||||
: "Add to\r\nWishlist";
|
||||
|
||||
public override bool Enabled
|
||||
{
|
||||
get => instanceEnabled;
|
||||
protected set
|
||||
{
|
||||
if (instanceEnabled != value)
|
||||
{
|
||||
instanceEnabled = value;
|
||||
OnPropertyChanged(nameof(Enabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool InWishList
|
||||
{
|
||||
get => inWishList;
|
||||
set
|
||||
{
|
||||
if (inWishList != value)
|
||||
{
|
||||
inWishList = value;
|
||||
OnPropertyChanged(nameof(InWishList));
|
||||
OnPropertyChanged(nameof(DisplayText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal WishlistButton(Item item, bool inLibrary, bool inWishList) : base(item, inLibrary)
|
||||
{
|
||||
this.inWishList = inWishList;
|
||||
}
|
||||
|
||||
public override async Task PerformClickAsync(LibraryBook accountBook)
|
||||
{
|
||||
if (!Enabled || !HasButtonAction) return;
|
||||
|
||||
Enabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
Api api = await accountBook.GetApiAsync();
|
||||
|
||||
if (InWishList)
|
||||
{
|
||||
await api.DeleteFromWishListAsync(Item.Asin);
|
||||
InWishList = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
await api.AddToWishListAsync(Item.Asin);
|
||||
InWishList = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var addRemove = InWishList ? "remove" : "add";
|
||||
var toFrom = InWishList ? "from" : "to";
|
||||
|
||||
Serilog.Log.Logger.Error(ex, $"Failed to {addRemove} {{book}} {toFrom} wish list", new { Item.ProductId, Item.TitleWithSubtitle });
|
||||
}
|
||||
finally { Enabled = true; }
|
||||
}
|
||||
|
||||
public override int CompareTo(object ob)
|
||||
{
|
||||
if (ob is not WishlistButton other) return -1;
|
||||
|
||||
int libcmp = other.InLibrary.CompareTo(InLibrary);
|
||||
return (libcmp == 0) ? other.InWishList.CompareTo(InWishList) : libcmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue