Add SeriesViewDialog
This commit is contained in:
parent
784ab73a36
commit
9ae1f0399b
@ -171,6 +171,59 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<int> ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(item, "item");
|
||||||
|
ArgumentValidator.EnsureNotNull(accountId, "accountId");
|
||||||
|
ArgumentValidator.EnsureNotNull(localeName, "localeName");
|
||||||
|
|
||||||
|
var importItem = new ImportItem
|
||||||
|
{
|
||||||
|
DtoItem = item,
|
||||||
|
AccountId = accountId,
|
||||||
|
LocaleName = localeName
|
||||||
|
};
|
||||||
|
|
||||||
|
var importItems = new List<ImportItem> { importItem };
|
||||||
|
var validator = new LibraryValidator();
|
||||||
|
var exceptions = validator.Validate(importItems.Select(i => i.DtoItem));
|
||||||
|
|
||||||
|
if (exceptions?.Any() ?? false)
|
||||||
|
{
|
||||||
|
Log.Logger.Error(new AggregateException(exceptions), "Error validating library book. {@DebugInfo}", new { item, accountId, localeName });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var context = DbContexts.GetContext();
|
||||||
|
|
||||||
|
var bookImporter = new BookImporter(context);
|
||||||
|
await Task.Run(() => bookImporter.Import(importItems));
|
||||||
|
var book = await Task.Run(() => context.LibraryBooks.FirstOrDefault(lb => lb.Book.AudibleProductId == importItem.DtoItem.ProductId));
|
||||||
|
|
||||||
|
if (book is null)
|
||||||
|
{
|
||||||
|
book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId);
|
||||||
|
context.LibraryBooks.Add(book);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
book.AbsentFromLastScan = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int qtyChanged = await Task.Run(() => SaveContext(context));
|
||||||
|
if (qtyChanged > 0)
|
||||||
|
await Task.Run(finalizeLibrarySizeChange);
|
||||||
|
return qtyChanged;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Logger.Error(ex, "Error adding single library book to DB. {@DebugInfo}", new { item, accountId, localeName });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||||
{
|
{
|
||||||
var tasks = new List<Task<List<ImportItem>>>();
|
var tasks = new List<Task<List<ImportItem>>>();
|
||||||
|
|||||||
@ -149,7 +149,7 @@ namespace AudibleUtilities
|
|||||||
foreach (var parent in items.Where(i => i.IsSeriesParent))
|
foreach (var parent in items.Where(i => i.IsSeriesParent))
|
||||||
{
|
{
|
||||||
var children = items.Where(i => i.IsEpisodes && i.Relationships.Any(r => r.Asin == parent.Asin));
|
var children = items.Where(i => i.IsEpisodes && i.Relationships.Any(r => r.Asin == parent.Asin));
|
||||||
setSeries(parent, children);
|
SetSeries(parent, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
@ -232,7 +232,7 @@ namespace AudibleUtilities
|
|||||||
finally { semaphore.Release(); }
|
finally { semaphore.Release(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setSeries(Item parent, IEnumerable<Item> children)
|
public static void SetSeries(Item parent, IEnumerable<Item> children)
|
||||||
{
|
{
|
||||||
//A series parent will always have exactly 1 Series
|
//A series parent will always have exactly 1 Series
|
||||||
parent.Series = new[]
|
parent.Series = new[]
|
||||||
|
|||||||
@ -8,7 +8,7 @@ namespace DataLayer
|
|||||||
internal int BookId { get; private set; }
|
internal int BookId { get; private set; }
|
||||||
|
|
||||||
public string Order { get; private set; }
|
public string Order { get; private set; }
|
||||||
public float Index { get; }
|
public float Index => StringLib.ExtractFirstNumber(Order);
|
||||||
|
|
||||||
public Series Series { get; private set; }
|
public Series Series { get; private set; }
|
||||||
public Book Book { get; private set; }
|
public Book Book { get; private set; }
|
||||||
@ -22,7 +22,6 @@ namespace DataLayer
|
|||||||
Series = series;
|
Series = series;
|
||||||
Book = book;
|
Book = book;
|
||||||
Order = order;
|
Order = order;
|
||||||
Index = StringLib.ExtractFirstNumber(Order);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateOrder(string order)
|
public void UpdateOrder(string order)
|
||||||
|
|||||||
@ -101,6 +101,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation UI", "Libation UI"
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation CLI", "Libation CLI", "{47E27674-595D-4F7A-8CFB-127E768E1D1E}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation CLI", "Libation CLI", "{47E27674-595D-4F7A-8CFB-127E768E1D1E}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi", "..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj", "{DF6FBE88-A9A0-4CED-86B4-F35A130F349D}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi.Common", "..\..\audible api\AudibleApi\AudibleApi.Common\AudibleApi.Common.csproj", "{093E79B6-9A57-46FE-8406-DB16BADAD427}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -219,6 +223,14 @@ Global
|
|||||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E90C4651-AF11-41B4-A839-10082D0391F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{DF6FBE88-A9A0-4CED-86B4-F35A130F349D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{DF6FBE88-A9A0-4CED-86B4-F35A130F349D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DF6FBE88-A9A0-4CED-86B4-F35A130F349D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{DF6FBE88-A9A0-4CED-86B4-F35A130F349D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{093E79B6-9A57-46FE-8406-DB16BADAD427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{093E79B6-9A57-46FE-8406-DB16BADAD427}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{093E79B6-9A57-46FE-8406-DB16BADAD427}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{093E79B6-9A57-46FE-8406-DB16BADAD427}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@ -2,6 +2,7 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Media;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@ -14,7 +15,14 @@ namespace LibationAvalonia.Controls
|
|||||||
public LinkLabel()
|
public LinkLabel()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
Tapped += LinkLabel_Tapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LinkLabel_Tapped(object sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
Foreground = Brushes.Purple;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnPointerEntered(PointerEventArgs e)
|
protected override void OnPointerEntered(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
this.Cursor = HandCursor;
|
this.Cursor = HandCursor;
|
||||||
|
|||||||
@ -114,12 +114,20 @@ namespace LibationAvalonia.ViewModels
|
|||||||
seriesEntry.Liberate.Expanded = false;
|
seriesEntry.Liberate.Expanded = false;
|
||||||
|
|
||||||
geList.Add(seriesEntry);
|
geList.Add(seriesEntry);
|
||||||
geList.AddRange(seriesEntry.Children);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Create the filtered-in list before adding entries to avoid a refresh
|
//Create the filtered-in list before adding entries to avoid a refresh
|
||||||
FilteredInGridEntries = QueryResults(geList, FilterString);
|
FilteredInGridEntries = QueryResults(geList, FilterString);
|
||||||
SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded));
|
SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded));
|
||||||
|
|
||||||
|
//Add all children beneath their parent
|
||||||
|
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
|
||||||
|
{
|
||||||
|
var seriesIndex = SOURCE.IndexOf(series);
|
||||||
|
foreach (var child in series.Children)
|
||||||
|
SOURCE.Insert(++seriesIndex, child);
|
||||||
|
}
|
||||||
|
|
||||||
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
||||||
GridEntries_CollectionChanged();
|
GridEntries_CollectionChanged();
|
||||||
}
|
}
|
||||||
@ -253,13 +261,15 @@ namespace LibationAvalonia.ViewModels
|
|||||||
//Series exists. Create and add episode child then update the SeriesEntry
|
//Series exists. Create and add episode child then update the SeriesEntry
|
||||||
episodeEntry = new LibraryBookEntry<AvaloniaEntryStatus>(episodeBook, seriesEntry);
|
episodeEntry = new LibraryBookEntry<AvaloniaEntryStatus>(episodeBook, seriesEntry);
|
||||||
seriesEntry.Children.Add(episodeEntry);
|
seriesEntry.Children.Add(episodeEntry);
|
||||||
|
seriesEntry.Children.Sort((c1, c2) => c1.SeriesIndex.CompareTo(c2.SeriesIndex));
|
||||||
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
|
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
|
||||||
seriesEntry.UpdateLibraryBook(seriesBook);
|
seriesEntry.UpdateLibraryBook(seriesBook);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add episode to the grid beneath the parent
|
//Add episode to the grid beneath the parent
|
||||||
int seriesIndex = SOURCE.IndexOf(seriesEntry);
|
int seriesIndex = SOURCE.IndexOf(seriesEntry);
|
||||||
SOURCE.Insert(seriesIndex + 1, episodeEntry);
|
int episodeIndex = seriesEntry.Children.IndexOf(episodeEntry);
|
||||||
|
SOURCE.Insert(seriesIndex + 1 + episodeIndex, episodeEntry);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
existingEpisodeEntry.UpdateLibraryBook(episodeBook);
|
existingEpisodeEntry.UpdateLibraryBook(episodeBook);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
using LibationUiBase;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -21,6 +22,8 @@ namespace LibationAvalonia.Views
|
|||||||
App.OpenAsset("img-coverart-prod-unavailable_500x500.jpg").CopyTo(ms3);
|
App.OpenAsset("img-coverart-prod-unavailable_500x500.jpg").CopyTo(ms3);
|
||||||
PictureStorage.SetDefaultImage(PictureSize._500x500, ms3.ToArray());
|
PictureStorage.SetDefaultImage(PictureSize._500x500, ms3.ToArray());
|
||||||
PictureStorage.SetDefaultImage(PictureSize.Native, ms3.ToArray());
|
PictureStorage.SetDefaultImage(PictureSize.Native, ms3.ToArray());
|
||||||
|
|
||||||
|
BaseUtil.SetLoadImageDelegate(AvaloniaUtils.TryLoadImageOrDefault);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -215,6 +215,19 @@ namespace LibationAvalonia.Views
|
|||||||
bookRecordMenuItem.Click += async (_, _) => await new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window);
|
bookRecordMenuItem.Click += async (_, _) => await new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
#region View All Series
|
||||||
|
|
||||||
|
if (entry.Book.SeriesLink.Any())
|
||||||
|
{
|
||||||
|
var header = entry.Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
|
||||||
|
var viewSeriesMenuItem = new MenuItem { Header = header };
|
||||||
|
|
||||||
|
args.ContextMenuItems.Add(viewSeriesMenuItem);
|
||||||
|
|
||||||
|
viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entry.LibraryBook).Show();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
32
Source/LibationAvalonia/Views/SeriesViewDialog.axaml
Normal file
32
Source/LibationAvalonia/Views/SeriesViewDialog.axaml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="LibationAvalonia.Views.SeriesViewDialog"
|
||||||
|
Icon="/Assets/libation.ico"
|
||||||
|
Title="View All Items in Series">
|
||||||
|
|
||||||
|
<TabControl
|
||||||
|
Items="{Binding TabItems}"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<TabControl.Styles>
|
||||||
|
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||||
|
<Setter Property="Height" Value="23"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem">
|
||||||
|
<Setter Property="MinHeight" Value="40"/>
|
||||||
|
<Setter Property="Height" Value="40"/>
|
||||||
|
<Setter Property="Padding" Value="8,2,8,5"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem#Header TextBlock">
|
||||||
|
<Setter Property="MinHeight" Value="5"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="Padding" Value="20,5,20,5"/>
|
||||||
|
</Style>
|
||||||
|
</TabControl.Styles>
|
||||||
|
|
||||||
|
</TabControl>
|
||||||
|
</Window>
|
||||||
70
Source/LibationAvalonia/Views/SeriesViewDialog.axaml.cs
Normal file
70
Source/LibationAvalonia/Views/SeriesViewDialog.axaml.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using AudibleApi.Common;
|
||||||
|
using AudibleApi;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DataLayer;
|
||||||
|
using Dinah.Core;
|
||||||
|
using FileLiberator;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using LibationAvalonia.Dialogs;
|
||||||
|
using LibationUiBase.SeriesView;
|
||||||
|
using System;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace LibationAvalonia.Views
|
||||||
|
{
|
||||||
|
public partial class SeriesViewDialog : DialogWindow
|
||||||
|
{
|
||||||
|
private readonly LibraryBook LibraryBook;
|
||||||
|
public AvaloniaList<TabItem> TabItems { get; } = new();
|
||||||
|
public SeriesViewDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
{
|
||||||
|
TabItems.Add(new TabItem { Header = "This is a Header", FontSize = 14, Content = new TextBlock { Text = "Some Text" } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Loaded += SeriesViewDialog_Loaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SeriesViewDialog(LibraryBook libraryBook) : this()
|
||||||
|
{
|
||||||
|
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, "libraryBook");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SeriesViewDialog_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var seriesEntries = await SeriesItem.GetAllSeriesItemsAsync(LibraryBook);
|
||||||
|
|
||||||
|
foreach (var series in seriesEntries.Keys)
|
||||||
|
{
|
||||||
|
TabItems.Add(new TabItem
|
||||||
|
{
|
||||||
|
Header = series.Title,
|
||||||
|
FontSize = 14,
|
||||||
|
Content = new SeriesViewGrid(LibraryBook, series, seriesEntries[series])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error(ex, "Error loading searies info");
|
||||||
|
|
||||||
|
TabItems.Add(new TabItem
|
||||||
|
{
|
||||||
|
Header = "ERROR",
|
||||||
|
Content = new TextBlock { Text = "ERROR LOADING SERIES INFO\r\n\r\n" + ex.Message, Foreground = Brushes.Red }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
Source/LibationAvalonia/Views/SeriesViewGrid.axaml
Normal file
100
Source/LibationAvalonia/Views/SeriesViewGrid.axaml
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||||
|
x:Class="LibationAvalonia.Views.SeriesViewGrid">
|
||||||
|
|
||||||
|
<DataGrid
|
||||||
|
ClipboardCopyMode="IncludeHeader"
|
||||||
|
GridLinesVisibility="All"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
Items="{Binding SeriesEntries}"
|
||||||
|
CanUserSortColumns="True"
|
||||||
|
CanUserReorderColumns="True"
|
||||||
|
BorderThickness="3">
|
||||||
|
|
||||||
|
<DataGrid.Styles>
|
||||||
|
<Style Selector="DataGridCell">
|
||||||
|
<Setter Property="Height" Value="80"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridCell > Panel">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridCell > Panel > TextBlock">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
<Setter Property="Padding" Value="4"/>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.Styles>
|
||||||
|
|
||||||
|
<DataGrid.Columns>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Width="80" Header="Cover" CanUserSort="False">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Image
|
||||||
|
Tapped="Cover_Click"
|
||||||
|
Height="80"
|
||||||
|
Source="{Binding Cover}"
|
||||||
|
ToolTip.Tip="Click to see full size" />
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Width="Auto" Header="Series
Order" CanUserSort="True" SortMemberPath="Order">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Panel>
|
||||||
|
<TextBlock
|
||||||
|
Text="{Binding Order}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Panel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Width="Auto" Header="Availability" CanUserSort="True" SortMemberPath="Button">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Panel>
|
||||||
|
<Button
|
||||||
|
Padding="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Click="Availability_Click"
|
||||||
|
IsVisible="{Binding Button.HasButtonAction}"
|
||||||
|
IsEnabled="{Binding Button.Enabled}">
|
||||||
|
<TextBlock
|
||||||
|
Text="{Binding Button.DisplayText}"
|
||||||
|
TextAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Button>
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
IsVisible="{Binding !Button.HasButtonAction}"
|
||||||
|
Text="{Binding Button.DisplayText}" />
|
||||||
|
</Panel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn MinWidth="150" Width="*" Header="Title" CanUserSort="True" SortMemberPath="Title">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Panel ToolTip.Tip="Open Audible product page">
|
||||||
|
<controls:LinkLabel
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{Binding Title}"
|
||||||
|
Tapped="Title_Click" />
|
||||||
|
</Panel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</UserControl>
|
||||||
90
Source/LibationAvalonia/Views/SeriesViewGrid.axaml.cs
Normal file
90
Source/LibationAvalonia/Views/SeriesViewGrid.axaml.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using ApplicationServices;
|
||||||
|
using AudibleApi.Common;
|
||||||
|
using AudibleUtilities;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DataLayer;
|
||||||
|
using Dinah.Core;
|
||||||
|
using LibationAvalonia.Controls;
|
||||||
|
using LibationAvalonia.Dialogs;
|
||||||
|
using LibationFileManager;
|
||||||
|
using LibationUiBase.SeriesView;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LibationAvalonia.Views
|
||||||
|
{
|
||||||
|
public partial class SeriesViewGrid : UserControl
|
||||||
|
{
|
||||||
|
private ImageDisplayDialog imageDisplayDialog;
|
||||||
|
private readonly LibraryBook LibraryBook;
|
||||||
|
|
||||||
|
public AvaloniaList<SeriesItem> SeriesEntries { get; } = new();
|
||||||
|
|
||||||
|
public SeriesViewGrid()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SeriesViewGrid(LibraryBook libraryBook, Item series, List<SeriesItem> entries) : this()
|
||||||
|
{
|
||||||
|
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||||
|
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||||
|
ArgumentValidator.EnsureNotNull(entries, nameof(entries));
|
||||||
|
|
||||||
|
SeriesEntries.AddRange(entries.OrderBy(s => s.Order));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Availability_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is Button button && button.DataContext is SeriesItem sentry && sentry.Button.HasButtonAction)
|
||||||
|
{
|
||||||
|
await sentry.Button.PerformClickAsync(LibraryBook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void Title_Click(object sender, Avalonia.Input.TappedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is not LinkLabel label || label.DataContext is not SeriesItem sentry)
|
||||||
|
return;
|
||||||
|
|
||||||
|
sentry.ViewOnAudible(LibraryBook.Book.Locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cover_Click(object sender, Avalonia.Input.TappedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is not Image tblock || tblock.DataContext is not SeriesItem sentry)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Item libraryBook = sentry.Item;
|
||||||
|
|
||||||
|
if (imageDisplayDialog is null || !imageDisplayDialog.IsVisible)
|
||||||
|
{
|
||||||
|
imageDisplayDialog = new ImageDisplayDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
var picDef = new PictureDefinition(libraryBook.PictureLarge ?? libraryBook.PictureId, PictureSize.Native);
|
||||||
|
|
||||||
|
void PictureCached(object sender, PictureCachedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Definition.PictureId == picDef.PictureId)
|
||||||
|
imageDisplayDialog.SetCoverBytes(e.Picture);
|
||||||
|
|
||||||
|
PictureStorage.PictureCached -= PictureCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
PictureStorage.PictureCached += PictureCached;
|
||||||
|
(bool isDefault, byte[] initialImageBts) = PictureStorage.GetPicture(picDef);
|
||||||
|
var windowTitle = $"{libraryBook.Title} - Cover";
|
||||||
|
|
||||||
|
imageDisplayDialog.Title = windowTitle;
|
||||||
|
imageDisplayDialog.SetCoverBytes(initialImageBts);
|
||||||
|
|
||||||
|
if (!isDefault)
|
||||||
|
PictureStorage.PictureCached -= PictureCached;
|
||||||
|
|
||||||
|
if (!imageDisplayDialog.IsVisible)
|
||||||
|
imageDisplayDialog.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Dinah.Core.WindowsDesktop.Drawing;
|
using Dinah.Core.WindowsDesktop.Drawing;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
using LibationUiBase;
|
||||||
|
|
||||||
namespace LibationWinForms
|
namespace LibationWinForms
|
||||||
{
|
{
|
||||||
@ -20,6 +21,8 @@ namespace LibationWinForms
|
|||||||
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||||
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
|
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||||
|
|
||||||
|
BaseUtil.SetLoadImageDelegate(WinFormsUtil.TryLoadImageOrDefault);
|
||||||
|
|
||||||
// wire-up event to automatically download after scan.
|
// wire-up event to automatically download after scan.
|
||||||
// winforms only. this should NOT be allowed in cli
|
// winforms only. this should NOT be allowed in cli
|
||||||
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
|
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
|
||||||
|
|||||||
@ -41,7 +41,6 @@
|
|||||||
productsGrid.TabIndex = 0;
|
productsGrid.TabIndex = 0;
|
||||||
productsGrid.VisibleCountChanged += productsGrid_VisibleCountChanged;
|
productsGrid.VisibleCountChanged += productsGrid_VisibleCountChanged;
|
||||||
productsGrid.LiberateClicked += productsGrid_LiberateClicked;
|
productsGrid.LiberateClicked += productsGrid_LiberateClicked;
|
||||||
productsGrid.ConvertToMp3Clicked += productsGrid_ConvertToMp3Clicked;
|
|
||||||
productsGrid.CoverClicked += productsGrid_CoverClicked;
|
productsGrid.CoverClicked += productsGrid_CoverClicked;
|
||||||
productsGrid.DetailsClicked += productsGrid_DetailsClicked;
|
productsGrid.DetailsClicked += productsGrid_DetailsClicked;
|
||||||
productsGrid.DescriptionClicked += productsGrid_DescriptionClicked;
|
productsGrid.DescriptionClicked += productsGrid_DescriptionClicked;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using FileLiberator;
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
using LibationWinForms.Dialogs;
|
using LibationWinForms.Dialogs;
|
||||||
|
using LibationWinForms.SeriesView;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
@ -218,6 +219,20 @@ namespace LibationWinForms.GridView
|
|||||||
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this);
|
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
#region View All Series
|
||||||
|
|
||||||
|
if (entry.Book.SeriesLink.Any())
|
||||||
|
{
|
||||||
|
var header = entry.Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
|
||||||
|
|
||||||
|
var viewSeriesMenuItem = new ToolStripMenuItem { Text = header };
|
||||||
|
|
||||||
|
ctxMenu.Items.Add(viewSeriesMenuItem);
|
||||||
|
|
||||||
|
viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entry.LibraryBook).Show();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,12 +348,6 @@ namespace LibationWinForms.GridView
|
|||||||
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
|
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void productsGrid_ConvertToMp3Clicked(ILibraryBookEntry liveGridEntry)
|
|
||||||
{
|
|
||||||
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error)
|
|
||||||
ConvertToMp3Clicked?.Invoke(this, liveGridEntry.LibraryBook);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void productsGrid_RemovableCountChanged(object sender, EventArgs e)
|
private void productsGrid_RemovableCountChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
RemovableCountChanged?.Invoke(sender, productsGrid.GetAllBookEntries().Count(lbe => lbe.Remove is true));
|
RemovableCountChanged?.Invoke(sender, productsGrid.GetAllBookEntries().Count(lbe => lbe.Remove is true));
|
||||||
|
|||||||
@ -21,7 +21,6 @@ namespace LibationWinForms.GridView
|
|||||||
/// <summary>Number of visible rows has changed</summary>
|
/// <summary>Number of visible rows has changed</summary>
|
||||||
public event EventHandler<int> VisibleCountChanged;
|
public event EventHandler<int> VisibleCountChanged;
|
||||||
public event LibraryBookEntryClickedEventHandler LiberateClicked;
|
public event LibraryBookEntryClickedEventHandler LiberateClicked;
|
||||||
public event LibraryBookEntryClickedEventHandler ConvertToMp3Clicked;
|
|
||||||
public event GridEntryClickedEventHandler CoverClicked;
|
public event GridEntryClickedEventHandler CoverClicked;
|
||||||
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
||||||
public event GridEntryRectangleClickedEventHandler DescriptionClicked;
|
public event GridEntryRectangleClickedEventHandler DescriptionClicked;
|
||||||
@ -308,7 +307,8 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
//Add episode to the grid beneath the parent
|
//Add episode to the grid beneath the parent
|
||||||
int seriesIndex = bindingList.IndexOf(seriesEntry);
|
int seriesIndex = bindingList.IndexOf(seriesEntry);
|
||||||
bindingList.Insert(seriesIndex + 1, episodeEntry);
|
int episodeIndex = seriesEntry.Children.IndexOf(episodeEntry);
|
||||||
|
bindingList.Insert(seriesIndex + 1 + episodeIndex, episodeEntry);
|
||||||
|
|
||||||
if (seriesEntry.Liberate.Expanded)
|
if (seriesEntry.Liberate.Expanded)
|
||||||
bindingList.ExpandItem(seriesEntry);
|
bindingList.ExpandItem(seriesEntry);
|
||||||
|
|||||||
49
Source/LibationWinForms/SeriesView/DownloadButtonColumn.cs
Normal file
49
Source/LibationWinForms/SeriesView/DownloadButtonColumn.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using System.Windows.Forms.VisualStyles;
|
||||||
|
using LibationUiBase.SeriesView;
|
||||||
|
|
||||||
|
namespace LibationWinForms.SeriesView
|
||||||
|
{
|
||||||
|
public class DownloadButtonColumn : DataGridViewButtonColumn
|
||||||
|
{
|
||||||
|
public DownloadButtonColumn()
|
||||||
|
{
|
||||||
|
CellTemplate = new DownloadButtonColumnCell();
|
||||||
|
CellTemplate.Style.WrapMode = DataGridViewTriState.True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal class DownloadButtonColumnCell : DataGridViewButtonCell
|
||||||
|
{
|
||||||
|
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 SeriesButton sentry)
|
||||||
|
{
|
||||||
|
string cellValue = sentry.DisplayText;
|
||||||
|
if (!sentry.Enabled)
|
||||||
|
{
|
||||||
|
//Draw disabled button
|
||||||
|
Rectangle buttonArea = cellBounds;
|
||||||
|
Rectangle buttonAdjustment = BorderWidths(advancedBorderStyle);
|
||||||
|
buttonArea.X += buttonAdjustment.X;
|
||||||
|
buttonArea.Y += buttonAdjustment.Y;
|
||||||
|
buttonArea.Height -= buttonAdjustment.Height;
|
||||||
|
buttonArea.Width -= buttonAdjustment.Width;
|
||||||
|
ButtonRenderer.DrawButton(graphics, buttonArea, cellValue, cellStyle.Font, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak, focused: false, PushButtonState.Disabled);
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (sentry.HasButtonAction)
|
||||||
|
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, cellValue, cellValue, errorText, cellStyle, advancedBorderStyle, paintParts);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, DataGridViewPaintParts.Background | DataGridViewPaintParts.Border);
|
||||||
|
TextRenderer.DrawText(graphics, cellValue, cellStyle.Font, cellBounds, cellStyle.ForeColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, DataGridViewPaintParts.Background | DataGridViewPaintParts.Border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Source/LibationWinForms/SeriesView/SeriesEntryBindingList.cs
Normal file
46
Source/LibationWinForms/SeriesView/SeriesEntryBindingList.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using LibationUiBase.SeriesView;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LibationWinForms.SeriesView
|
||||||
|
{
|
||||||
|
internal class SeriesEntryBindingList : BindingList<SeriesItem>
|
||||||
|
{
|
||||||
|
private PropertyDescriptor _propertyDescriptor;
|
||||||
|
|
||||||
|
private ListSortDirection _listSortDirection;
|
||||||
|
|
||||||
|
private bool _isSortedCore;
|
||||||
|
|
||||||
|
protected override PropertyDescriptor SortPropertyCore => _propertyDescriptor;
|
||||||
|
|
||||||
|
protected override ListSortDirection SortDirectionCore => _listSortDirection;
|
||||||
|
|
||||||
|
protected override bool IsSortedCore => _isSortedCore;
|
||||||
|
|
||||||
|
protected override bool SupportsSortingCore => true;
|
||||||
|
|
||||||
|
public SeriesEntryBindingList() : base(new List<SeriesItem>()) { }
|
||||||
|
public SeriesEntryBindingList(IEnumerable<SeriesItem> entries) : base(entries.ToList()) { }
|
||||||
|
|
||||||
|
protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
|
||||||
|
{
|
||||||
|
var itemsList = (List<SeriesItem>)base.Items;
|
||||||
|
|
||||||
|
var sorted
|
||||||
|
= (direction == ListSortDirection.Ascending)
|
||||||
|
? itemsList.OrderBy(prop.GetValue).ToList()
|
||||||
|
: itemsList.OrderByDescending(prop.GetValue).ToList();
|
||||||
|
|
||||||
|
itemsList.Clear();
|
||||||
|
itemsList.AddRange(sorted);
|
||||||
|
|
||||||
|
_propertyDescriptor = prop;
|
||||||
|
_listSortDirection = direction;
|
||||||
|
_isSortedCore = true;
|
||||||
|
|
||||||
|
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
Source/LibationWinForms/SeriesView/SeriesViewDialog.Designer.cs
generated
Normal file
62
Source/LibationWinForms/SeriesView/SeriesViewDialog.Designer.cs
generated
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace LibationWinForms.SeriesView
|
||||||
|
{
|
||||||
|
partial class SeriesViewDialog
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Required designer variable.
|
||||||
|
/// </summary>
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clean up any resources being used.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing && (components != null))
|
||||||
|
{
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Form Designer generated code
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required method for Designer support - do not modify
|
||||||
|
/// the contents of this method with the code editor.
|
||||||
|
/// </summary>
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
tabControl1 = new TabControl();
|
||||||
|
SuspendLayout();
|
||||||
|
//
|
||||||
|
// tabControl1
|
||||||
|
//
|
||||||
|
tabControl1.Dock = DockStyle.Fill;
|
||||||
|
tabControl1.Location = new System.Drawing.Point(0, 0);
|
||||||
|
tabControl1.Name = "tabControl1";
|
||||||
|
tabControl1.SelectedIndex = 0;
|
||||||
|
tabControl1.Size = new System.Drawing.Size(800, 450);
|
||||||
|
tabControl1.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// SeriesViewDialog
|
||||||
|
//
|
||||||
|
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
|
AutoScaleMode = AutoScaleMode.Font;
|
||||||
|
ClientSize = new System.Drawing.Size(800, 450);
|
||||||
|
Controls.Add(tabControl1);
|
||||||
|
FormBorderStyle = FormBorderStyle.SizableToolWindow;
|
||||||
|
Name = "SeriesViewDialog";
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
Text = "View All Items in Series";
|
||||||
|
ResumeLayout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TabControl tabControl1;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
193
Source/LibationWinForms/SeriesView/SeriesViewDialog.cs
Normal file
193
Source/LibationWinForms/SeriesView/SeriesViewDialog.cs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
using AudibleApi.Common;
|
||||||
|
using DataLayer;
|
||||||
|
using Dinah.Core;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using System;
|
||||||
|
using Dinah.Core.WindowsDesktop.Forms;
|
||||||
|
using LibationWinForms.GridView;
|
||||||
|
using LibationFileManager;
|
||||||
|
using LibationUiBase.SeriesView;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace LibationWinForms.SeriesView
|
||||||
|
{
|
||||||
|
public partial class SeriesViewDialog : Form
|
||||||
|
{
|
||||||
|
private readonly LibraryBook LibraryBook;
|
||||||
|
|
||||||
|
public SeriesViewDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||||
|
this.SetLibationIcon();
|
||||||
|
|
||||||
|
Load += SeriesViewDialog_Load;
|
||||||
|
FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SeriesViewDialog(LibraryBook libraryBook) : this()
|
||||||
|
{
|
||||||
|
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, "libraryBook");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SeriesViewDialog_Load(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var seriesEntries = await SeriesItem.GetAllSeriesItemsAsync(LibraryBook);
|
||||||
|
|
||||||
|
//Create a DataGridView for each series and add all children of that series to it.
|
||||||
|
foreach (var series in seriesEntries.Keys)
|
||||||
|
{
|
||||||
|
var dgv = createNewSeriesGrid();
|
||||||
|
dgv.CellContentClick += Dgv_CellContentClick;
|
||||||
|
dgv.DataSource = new SeriesEntryBindingList(seriesEntries[series]);
|
||||||
|
dgv.BindingContextChanged += (_, _) => dgv.Sort(dgv.Columns["Order"], ListSortDirection.Ascending);
|
||||||
|
|
||||||
|
var tab = new TabPage { Text = series.Title };
|
||||||
|
tab.Controls.Add(dgv);
|
||||||
|
tab.VisibleChanged += (_, _) => dgv.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
|
||||||
|
tabControl1.Controls.Add(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error(ex, "Error loading searies info");
|
||||||
|
|
||||||
|
var tab = new TabPage { Text = "ERROR" };
|
||||||
|
tab.Controls.Add(new Label { Text = "ERROR LOADING SERIES INFO\r\n\r\n" + ex.Message, ForeColor = Color.Red, Dock = DockStyle.Fill });
|
||||||
|
tabControl1.Controls.Add(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageDisplay imageDisplay;
|
||||||
|
|
||||||
|
private async void Dgv_CellContentClick(object sender, DataGridViewCellEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.RowIndex < 0) return;
|
||||||
|
|
||||||
|
var dgv = (DataGridView)sender;
|
||||||
|
var sentry = dgv.GetBoundItem<SeriesItem>(e.RowIndex);
|
||||||
|
|
||||||
|
if (dgv.Columns[e.ColumnIndex].DataPropertyName == nameof(SeriesItem.Cover))
|
||||||
|
{
|
||||||
|
coverClicked(sentry.Item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (dgv.Columns[e.ColumnIndex].DataPropertyName == nameof(SeriesItem.Title))
|
||||||
|
{
|
||||||
|
sentry.ViewOnAudible(LibraryBook.Book.Locale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (dgv.Columns[e.ColumnIndex].DataPropertyName == nameof(SeriesItem.Button) && sentry.Button.HasButtonAction)
|
||||||
|
{
|
||||||
|
await sentry.Button.PerformClickAsync(LibraryBook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void coverClicked(Item libraryBook)
|
||||||
|
{
|
||||||
|
var picDef = new PictureDefinition(libraryBook.PictureLarge ?? libraryBook.PictureId, PictureSize.Native);
|
||||||
|
|
||||||
|
void PictureCached(object sender, PictureCachedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Definition.PictureId == picDef.PictureId)
|
||||||
|
imageDisplay.SetCoverArt(e.Picture);
|
||||||
|
|
||||||
|
PictureStorage.PictureCached -= PictureCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
PictureStorage.PictureCached += PictureCached;
|
||||||
|
(bool isDefault, byte[] initialImageBts) = PictureStorage.GetPicture(picDef);
|
||||||
|
|
||||||
|
var windowTitle = $"{libraryBook.Title} - Cover";
|
||||||
|
|
||||||
|
if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
|
||||||
|
{
|
||||||
|
imageDisplay = new ImageDisplay();
|
||||||
|
imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
|
||||||
|
imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
imageDisplay.Text = windowTitle;
|
||||||
|
imageDisplay.SetCoverArt(initialImageBts);
|
||||||
|
if (!isDefault)
|
||||||
|
PictureStorage.PictureCached -= PictureCached;
|
||||||
|
|
||||||
|
if (!imageDisplay.Visible)
|
||||||
|
imageDisplay.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DataGridView createNewSeriesGrid()
|
||||||
|
{
|
||||||
|
var dgv = new DataGridView
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
RowHeadersVisible = false,
|
||||||
|
ReadOnly = false,
|
||||||
|
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize,
|
||||||
|
AllowUserToAddRows = false,
|
||||||
|
AllowUserToDeleteRows = false,
|
||||||
|
AllowUserToResizeRows = false,
|
||||||
|
AutoGenerateColumns = false
|
||||||
|
};
|
||||||
|
|
||||||
|
dgv.RowTemplate.Height = 80;
|
||||||
|
|
||||||
|
dgv.Columns.Add(new DataGridViewImageColumn
|
||||||
|
{
|
||||||
|
DataPropertyName = nameof(SeriesItem.Cover),
|
||||||
|
HeaderText = "Cover",
|
||||||
|
Name = "Cover",
|
||||||
|
ReadOnly = true,
|
||||||
|
Resizable = DataGridViewTriState.False,
|
||||||
|
Width = 80
|
||||||
|
});
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn
|
||||||
|
{
|
||||||
|
DataPropertyName = nameof(SeriesItem.Order),
|
||||||
|
HeaderText = "Series\r\nOrder",
|
||||||
|
Name = "Order",
|
||||||
|
ReadOnly = true,
|
||||||
|
SortMode = DataGridViewColumnSortMode.Automatic,
|
||||||
|
Width = 50
|
||||||
|
});
|
||||||
|
dgv.Columns.Add(new DownloadButtonColumn
|
||||||
|
{
|
||||||
|
DataPropertyName = nameof(SeriesItem.Button),
|
||||||
|
HeaderText = "Availability",
|
||||||
|
Name = "DownloadButton",
|
||||||
|
ReadOnly = true,
|
||||||
|
SortMode = DataGridViewColumnSortMode.Automatic,
|
||||||
|
Width = 50
|
||||||
|
});
|
||||||
|
dgv.Columns.Add(new DataGridViewLinkColumn
|
||||||
|
{
|
||||||
|
DataPropertyName = nameof(SeriesItem.Title),
|
||||||
|
HeaderText = "Title",
|
||||||
|
Name = "Title",
|
||||||
|
ReadOnly = true,
|
||||||
|
TrackVisitedState = true,
|
||||||
|
SortMode = DataGridViewColumnSortMode.Automatic,
|
||||||
|
Width = 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
dgv.CellToolTipTextNeeded += Dgv_CellToolTipTextNeeded;
|
||||||
|
|
||||||
|
return dgv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Dgv_CellToolTipTextNeeded(object sender, DataGridViewCellToolTipTextNeededEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not DataGridView dgv || e.ColumnIndex < 0) return;
|
||||||
|
|
||||||
|
e.ToolTipText = dgv.Columns[e.ColumnIndex].DataPropertyName switch
|
||||||
|
{
|
||||||
|
nameof(SeriesItem.Cover) => "Click to see full size",
|
||||||
|
nameof(SeriesItem.Title) => "Open Audible product page",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
Source/LibationWinForms/SeriesView/SeriesViewDialog.resx
Normal file
60
Source/LibationWinForms/SeriesView/SeriesViewDialog.resx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
||||||
Loading…
x
Reference in New Issue
Block a user