Add SeriesViewDialog
This commit is contained in:
parent
784ab73a36
commit
9ae1f0399b
@ -171,7 +171,60 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
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)
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
|
||||
|
||||
@ -149,7 +149,7 @@ namespace AudibleUtilities
|
||||
foreach (var parent in items.Where(i => i.IsSeriesParent))
|
||||
{
|
||||
var children = items.Where(i => i.IsEpisodes && i.Relationships.Any(r => r.Asin == parent.Asin));
|
||||
setSeries(parent, children);
|
||||
SetSeries(parent, children);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
@ -232,7 +232,7 @@ namespace AudibleUtilities
|
||||
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
|
||||
parent.Series = new[]
|
||||
@ -267,4 +267,4 @@ namespace AudibleUtilities
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ namespace DataLayer
|
||||
internal int BookId { 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 Book Book { get; private set; }
|
||||
@ -22,8 +22,7 @@ namespace DataLayer
|
||||
Series = series;
|
||||
Book = book;
|
||||
Order = order;
|
||||
Index = StringLib.ExtractFirstNumber(Order);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateOrder(string order)
|
||||
{
|
||||
|
||||
@ -101,6 +101,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation UI", "Libation UI"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation CLI", "Libation CLI", "{47E27674-595D-4F7A-8CFB-127E768E1D1E}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@ -2,6 +2,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
|
||||
@ -14,7 +15,14 @@ namespace LibationAvalonia.Controls
|
||||
public LinkLabel()
|
||||
{
|
||||
InitializeComponent();
|
||||
Tapped += LinkLabel_Tapped;
|
||||
}
|
||||
|
||||
private void LinkLabel_Tapped(object sender, TappedEventArgs e)
|
||||
{
|
||||
Foreground = Brushes.Purple;
|
||||
}
|
||||
|
||||
protected override void OnPointerEntered(PointerEventArgs e)
|
||||
{
|
||||
this.Cursor = HandCursor;
|
||||
|
||||
@ -114,12 +114,20 @@ namespace LibationAvalonia.ViewModels
|
||||
seriesEntry.Liberate.Expanded = false;
|
||||
|
||||
geList.Add(seriesEntry);
|
||||
geList.AddRange(seriesEntry.Children);
|
||||
}
|
||||
|
||||
//Create the filtered-in list before adding entries to avoid a refresh
|
||||
FilteredInGridEntries = QueryResults(geList, FilterString);
|
||||
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();
|
||||
}
|
||||
@ -253,13 +261,15 @@ namespace LibationAvalonia.ViewModels
|
||||
//Series exists. Create and add episode child then update the SeriesEntry
|
||||
episodeEntry = new LibraryBookEntry<AvaloniaEntryStatus>(episodeBook, seriesEntry);
|
||||
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);
|
||||
seriesEntry.UpdateLibraryBook(seriesBook);
|
||||
}
|
||||
|
||||
//Add episode to the grid beneath the parent
|
||||
int seriesIndex = SOURCE.IndexOf(seriesEntry);
|
||||
SOURCE.Insert(seriesIndex + 1, episodeEntry);
|
||||
int episodeIndex = seriesEntry.Children.IndexOf(episodeEntry);
|
||||
SOURCE.Insert(seriesIndex + 1 + episodeIndex, episodeEntry);
|
||||
}
|
||||
else
|
||||
existingEpisodeEntry.UpdateLibraryBook(episodeBook);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -21,6 +22,8 @@ namespace LibationAvalonia.Views
|
||||
App.OpenAsset("img-coverart-prod-unavailable_500x500.jpg").CopyTo(ms3);
|
||||
PictureStorage.SetDefaultImage(PictureSize._500x500, 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);
|
||||
}
|
||||
|
||||
#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
|
||||
}
|
||||
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 Dinah.Core.WindowsDesktop.Drawing;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
@ -20,6 +21,8 @@ namespace LibationWinForms
|
||||
PictureStorage.SetDefaultImage(PictureSize._500x500, 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.
|
||||
// winforms only. this should NOT be allowed in cli
|
||||
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
|
||||
|
||||
@ -41,7 +41,6 @@
|
||||
productsGrid.TabIndex = 0;
|
||||
productsGrid.VisibleCountChanged += productsGrid_VisibleCountChanged;
|
||||
productsGrid.LiberateClicked += productsGrid_LiberateClicked;
|
||||
productsGrid.ConvertToMp3Clicked += productsGrid_ConvertToMp3Clicked;
|
||||
productsGrid.CoverClicked += productsGrid_CoverClicked;
|
||||
productsGrid.DetailsClicked += productsGrid_DetailsClicked;
|
||||
productsGrid.DescriptionClicked += productsGrid_DescriptionClicked;
|
||||
|
||||
@ -5,6 +5,7 @@ using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.GridView;
|
||||
using LibationWinForms.Dialogs;
|
||||
using LibationWinForms.SeriesView;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
@ -218,6 +219,20 @@ namespace LibationWinForms.GridView
|
||||
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
|
||||
}
|
||||
|
||||
@ -333,12 +348,6 @@ namespace LibationWinForms.GridView
|
||||
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)
|
||||
{
|
||||
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>
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event LibraryBookEntryClickedEventHandler LiberateClicked;
|
||||
public event LibraryBookEntryClickedEventHandler ConvertToMp3Clicked;
|
||||
public event GridEntryClickedEventHandler CoverClicked;
|
||||
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
||||
public event GridEntryRectangleClickedEventHandler DescriptionClicked;
|
||||
@ -308,7 +307,8 @@ namespace LibationWinForms.GridView
|
||||
|
||||
//Add episode to the grid beneath the parent
|
||||
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)
|
||||
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