Add multi-select context menu support (rmcrackan/Libation#1195)

This commit is contained in:
Michael Bucari-Tovo 2025-03-21 16:38:55 -06:00
parent bfcd226795
commit c77f2e2162
10 changed files with 455 additions and 229 deletions

View File

@ -2,6 +2,7 @@
using Avalonia.Controls; using Avalonia.Controls;
using LibationUiBase.GridView; using LibationUiBase.GridView;
using System; using System;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace LibationAvalonia.Controls namespace LibationAvalonia.Controls
@ -12,11 +13,13 @@ namespace LibationAvalonia.Controls
private static readonly ContextMenu ContextMenu = new(); private static readonly ContextMenu ContextMenu = new();
private static readonly AvaloniaList<Control> MenuItems = new(); private static readonly AvaloniaList<Control> MenuItems = new();
private static readonly PropertyInfo OwningColumnProperty; private static readonly PropertyInfo OwningColumnProperty;
private static readonly PropertyInfo OwningGridProperty;
static DataGridContextMenus() static DataGridContextMenus()
{ {
ContextMenu.ItemsSource = MenuItems; ContextMenu.ItemsSource = MenuItems;
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic); OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic);
OwningGridProperty = typeof(DataGridColumn).GetProperty("OwningGrid", BindingFlags.Instance | BindingFlags.NonPublic);
} }
public static void AttachContextMenu(this DataGridCell cell) public static void AttachContextMenu(this DataGridCell cell)
@ -30,19 +33,35 @@ namespace LibationAvalonia.Controls
private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e) private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e)
{ {
if (sender is DataGridCell cell && cell.DataContext is IGridEntry entry) if (sender is DataGridCell cell &&
cell.DataContext is IGridEntry clickedEntry &&
OwningColumnProperty.GetValue(cell) is DataGridColumn column &&
OwningGridProperty.GetValue(column) is DataGrid grid)
{ {
var allSelected = grid.SelectedItems.OfType<IGridEntry>().ToArray();
var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
if (clickedIndex == -1)
{
//User didn't right-click on a selected cell
grid.SelectedItem = clickedEntry;
allSelected = [clickedEntry];
}
else if (clickedIndex > 0)
{
//Ensure the clicked entry is first in the list
(allSelected[0], allSelected[clickedIndex]) = (allSelected[clickedIndex], allSelected[0]);
}
var args = new DataGridCellContextMenuStripNeededEventArgs var args = new DataGridCellContextMenuStripNeededEventArgs
{ {
Column = OwningColumnProperty.GetValue(cell) as DataGridColumn, Column = column,
GridEntry = entry, Grid = grid,
GridEntries = allSelected,
ContextMenu = ContextMenu ContextMenu = ContextMenu
}; };
args.ContextMenuItems.Clear(); args.ContextMenuItems.Clear();
CellContextMenuStripNeeded?.Invoke(sender, args); CellContextMenuStripNeeded?.Invoke(sender, args);
e.Handled = args.ContextMenuItems.Count == 0; e.Handled = args.ContextMenuItems.Count == 0;
} }
else else
@ -61,10 +80,37 @@ namespace LibationAvalonia.Controls
private static string GetCellValue(DataGridColumn column, object item) private static string GetCellValue(DataGridColumn column, object item)
=> GetCellValueMethod.Invoke(column, new object[] { item, column.ClipboardContentBinding })?.ToString() ?? ""; => GetCellValueMethod.Invoke(column, new object[] { item, column.ClipboardContentBinding })?.ToString() ?? "";
public string CellClipboardContents => GetCellValue(Column, GridEntry); public string CellClipboardContents => GetCellValue(Column, GridEntries[0]);
public DataGridColumn Column { get; init; } public string GetRowClipboardContents()
public IGridEntry GridEntry { get; init; } {
public ContextMenu ContextMenu { get; init; } if (GridEntries is null || GridEntries.Length == 0)
return string.Empty;
else if (GridEntries.Length == 1)
return HeaderNames + Environment.NewLine + GetRowClipboardContents(GridEntries[0]);
else
return string.Join(Environment.NewLine, GridEntries.Select(GetRowClipboardContents).Prepend(HeaderNames));
}
private string HeaderNames
=> string.Join("\t",
Grid.Columns
.Where(c => c.IsVisible)
.OrderBy(c => c.DisplayIndex)
.Select(c => RemoveLineBreaks(c.Header.ToString())));
private static string RemoveLineBreaks(string text)
=> text.Replace("\r\n", "").Replace('\r', ' ').Replace('\n', ' ');
private string GetRowClipboardContents(IGridEntry gridEntry)
{
var contents = Grid.Columns.Where(c => c.IsVisible).OrderBy(c => c.DisplayIndex).Select(c => RemoveLineBreaks(GetCellValue(c, gridEntry))).ToArray();
return string.Join("\t", contents);
}
public required DataGrid Grid { get; init; }
public required DataGridColumn Column { get; init; }
public required IGridEntry[] GridEntries { get; init; }
public required ContextMenu ContextMenu { get; init; }
public AvaloniaList<Control> ContextMenuItems public AvaloniaList<Control> ContextMenuItems
=> ContextMenu.ItemsSource as AvaloniaList<Control>; => ContextMenu.ItemsSource as AvaloniaList<Control>;
} }

View File

@ -33,37 +33,54 @@ namespace LibationAvalonia.ViewModels
setQueueCollapseState(collapseState); setQueueCollapseState(collapseState);
} }
public async void LiberateClicked(LibraryBook libraryBook) public async void LiberateClicked(LibraryBook[] libraryBooks)
{ {
try try
{ {
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload) if (libraryBooks.Length == 1)
{ {
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", libraryBook); var item = libraryBooks[0];
setQueueCollapseState(false); if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
ProcessQueue.AddDownloadDecrypt(libraryBook);
}
else if (libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
setQueueCollapseState(false);
ProcessQueue.AddDownloadPdf(libraryBook);
}
else if (libraryBook.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath?.ShortPathName))
{ {
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}"; Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", item);
await MessageBox.Show($"File not found" + suffix); setQueueCollapseState(false);
ProcessQueue.AddDownloadDecrypt(item);
}
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
setQueueCollapseState(false);
ProcessQueue.AddDownloadPdf(item);
}
else if (item.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(item.Book.AudibleProductId);
if (!Go.To.File(filePath?.ShortPathName))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
await MessageBox.Show($"File not found" + suffix);
}
}
}
else
{
var toLiberate
= libraryBooks
.Where(x => x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
.ToArray();
if (toLiberate.Length > 0)
{
setQueueCollapseState(false);
ProcessQueue.AddDownloadDecrypt(toLiberate);
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook); Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
} }
} }
@ -83,20 +100,21 @@ namespace LibationAvalonia.ViewModels
} }
} }
public void ConvertToMp3Clicked(LibraryBook libraryBook) public void ConvertToMp3Clicked(LibraryBook[] libraryBooks)
{ {
try try
{ {
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated) var preLiberated = libraryBooks.Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated).ToArray();
if (preLiberated.Length > 0)
{ {
Serilog.Log.Logger.Information("Begin convert to mp3 {libraryBook}", libraryBook); Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
setQueueCollapseState(false); setQueueCollapseState(false);
ProcessQueue.AddConvertMp3(libraryBook); ProcessQueue.AddConvertMp3(preLiberated);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook); Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
} }
} }

View File

@ -91,19 +91,19 @@ namespace LibationAvalonia.ViewModels
public decimal SpeedLimitIncrement { get; private set; } public decimal SpeedLimitIncrement { get; private set; }
private async void Queue_CompletedCountChanged(object? sender, int e) private void Queue_CompletedCountChanged(object? sender, int e)
{ {
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail); int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success); int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
ErrorCount = errCount; ErrorCount = errCount;
CompletedCount = completeCount; CompletedCount = completeCount;
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress))); Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
} }
private async void Queue_QueuededCountChanged(object? sender, int cueCount) private void Queue_QueuededCountChanged(object? sender, int cueCount)
{ {
QueuedCount = cueCount; QueuedCount = cueCount;
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress))); Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
} }
public void WriteLine(string text) public void WriteLine(string text)

View File

@ -134,9 +134,9 @@ namespace LibationAvalonia.Views
Task.Run(() => vm.ProductsDisplay.BindToGridAsync(initialLibrary))); Task.Run(() => vm.ProductsDisplay.BindToGridAsync(initialLibrary)));
} }
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook); public void ProductsDisplay_LiberateClicked(object _, LibraryBook[] libraryBook) => ViewModel.LiberateClicked(libraryBook);
public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series); public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook); public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook[] libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
BookDetailsDialog bookDetailsForm; BookDetailsDialog bookDetailsForm;
public void ProductsDisplay_TagsButtonClicked(object _, LibraryBook libraryBook) public void ProductsDisplay_TagsButtonClicked(object _, LibraryBook libraryBook)

View File

@ -24,9 +24,9 @@ namespace LibationAvalonia.Views
{ {
public partial class ProductsDisplay : UserControl public partial class ProductsDisplay : UserControl
{ {
public event EventHandler<LibraryBook>? LiberateClicked; public event EventHandler<LibraryBook[]>? LiberateClicked;
public event EventHandler<ISeriesEntry>? LiberateSeriesClicked; public event EventHandler<ISeriesEntry>? LiberateSeriesClicked;
public event EventHandler<LibraryBook>? ConvertToMp3Clicked; public event EventHandler<LibraryBook[]>? ConvertToMp3Clicked;
public event EventHandler<LibraryBook>? TagsButtonClicked; public event EventHandler<LibraryBook>? TagsButtonClicked;
private ProductsDisplayViewModel? _viewModel => DataContext as ProductsDisplayViewModel; private ProductsDisplayViewModel? _viewModel => DataContext as ProductsDisplayViewModel;
@ -191,29 +191,41 @@ namespace LibationAvalonia.Views
public void ProductsGrid_CellContextMenuStripNeeded(object? sender, DataGridCellContextMenuStripNeededEventArgs args) public void ProductsGrid_CellContextMenuStripNeeded(object? sender, DataGridCellContextMenuStripNeededEventArgs args)
{ {
var entry = args.GridEntry; var entries = args.GridEntries;
var ctx = new GridContextMenu(entry, '_'); var ctx = new GridContextMenu(entries, '_');
if (args.Column.SortMemberPath is not "Liberate" and not "Cover" if (App.MainWindow?.Clipboard is IClipboard clipboard)
&& App.MainWindow?.Clipboard is IClipboard clipboard)
{ {
//Avalonia's DataGrid can't select individual cells, so add separate
//options for copying single cell's contents and who row contents.
if (entries.Length == 1 && args.Column.SortMemberPath is not "Liberate" and not "Cover")
{
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.CopyCellText,
Command = ReactiveCommand.CreateFromTask(() => clipboard?.SetTextAsync(args.CellClipboardContents) ?? Task.CompletedTask)
});
}
args.ContextMenuItems.Add(new MenuItem args.ContextMenuItems.Add(new MenuItem
{ {
Header = ctx.CopyCellText, Header = "_Copy Row Contents",
Command = ReactiveCommand.CreateFromTask(() => clipboard?.SetTextAsync(args.CellClipboardContents) ?? Task.CompletedTask) Command = ReactiveCommand.CreateFromTask(() => clipboard?.SetTextAsync(args.GetRowClipboardContents()) ?? Task.CompletedTask)
}); });
args.ContextMenuItems.Add(new Separator()); args.ContextMenuItems.Add(new Separator());
} }
#region Liberate all Episodes #region Liberate all Episodes (Single series only)
if (entry.Liberate.IsSeries) if (entries.Length == 1 && entries[0] is ISeriesEntry seriesEntry)
{ {
args.ContextMenuItems.Add(new MenuItem() args.ContextMenuItems.Add(new MenuItem()
{ {
Header = ctx.LiberateEpisodesText, Header = ctx.LiberateEpisodesText,
IsEnabled = ctx.LiberateEpisodesEnabled, IsEnabled = ctx.LiberateEpisodesEnabled,
Command = ReactiveCommand.Create(() => LiberateSeriesClicked?.Invoke(this, (ISeriesEntry)entry)) Command = ReactiveCommand.Create(() => LiberateSeriesClicked?.Invoke(this, seriesEntry))
}); });
} }
@ -238,20 +250,10 @@ namespace LibationAvalonia.Views
}); });
#endregion #endregion
#region Remove from library #region Locate file (Single book only)
args.ContextMenuItems.Add(new MenuItem if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry)
{ {
Header = ctx.RemoveText,
Command = ReactiveCommand.CreateFromTask(ctx.RemoveAsync)
});
#endregion
if (!entry.Liberate.IsSeries)
{
#region Locate file
args.ContextMenuItems.Add(new MenuItem args.ContextMenuItems.Add(new MenuItem
{ {
Header = ctx.LocateFileText, Header = ctx.LocateFileText,
@ -285,21 +287,44 @@ namespace LibationAvalonia.Views
} }
}) })
}); });
}
#endregion #endregion
#region Convert to Mp3 #region Remove from library
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.RemoveText,
Command = ReactiveCommand.CreateFromTask(ctx.RemoveAsync)
});
#endregion
#region Liberate All (multiple books only)
if (entries.OfType<ILibraryBookEntry>().Count() > 1)
{
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.DownloadSelectedText,
Command = ReactiveCommand.Create(() => LiberateClicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray()))
});
}
#endregion
#region Convert to Mp3
if (ctx.LibraryBookEntries.Length > 0)
{
args.ContextMenuItems.Add(new MenuItem args.ContextMenuItems.Add(new MenuItem
{ {
Header = ctx.ConvertToMp3Text, Header = ctx.ConvertToMp3Text,
IsEnabled = ctx.ConvertToMp3Enabled, IsEnabled = ctx.ConvertToMp3Enabled,
Command = ReactiveCommand.Create(() => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook)) Command = ReactiveCommand.Create(() => ConvertToMp3Clicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray()))
}); });
#endregion
} }
#region Force Re-Download #endregion
if (!entry.Liberate.IsSeries) #region Force Re-Download (Single book only)
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry4)
{ {
args.ContextMenuItems.Add(new MenuItem() args.ContextMenuItems.Add(new MenuItem()
{ {
@ -307,17 +332,23 @@ namespace LibationAvalonia.Views
IsEnabled = ctx.ReDownloadEnabled, IsEnabled = ctx.ReDownloadEnabled,
Command = ReactiveCommand.Create(() => Command = ReactiveCommand.Create(() =>
{ {
//No need to persist this change. It only needs to last long for the file to start downloading //No need to persist these changes. They only needs to last long for the files to start downloading
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated; entry4.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
LiberateClicked?.Invoke(this, entry.LibraryBook); if (entry4.Book.HasPdf())
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
LiberateClicked?.Invoke(this, [entry4.LibraryBook]);
}) })
}); });
} }
#endregion #endregion
if (entries.Length > 1)
return;
args.ContextMenuItems.Add(new Separator()); args.ContextMenuItems.Add(new Separator());
#region Edit Templates #region Edit Templates (Single book only)
async Task editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate) async Task editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate)
where T : Templates, LibationFileManager.ITemplate, new() where T : Templates, LibationFileManager.ITemplate, new()
{ {
@ -329,7 +360,7 @@ namespace LibationAvalonia.Views
} }
} }
if (!entry.Liberate.IsSeries) if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry2)
{ {
args.ContextMenuItems.Add(new MenuItem args.ContextMenuItems.Add(new MenuItem
{ {
@ -339,17 +370,17 @@ namespace LibationAvalonia.Views
new MenuItem new MenuItem
{ {
Header = ctx.FolderTemplateText, Header = ctx.FolderTemplateText,
Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.FolderTemplate>(entry.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t)) Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.FolderTemplate>(entry2.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t))
}, },
new MenuItem new MenuItem
{ {
Header = ctx.FileTemplateText, Header = ctx.FileTemplateText,
Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.FileTemplate>(entry.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t)) Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.FileTemplate>(entry2.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t))
}, },
new MenuItem new MenuItem
{ {
Header = ctx.MultipartTemplateText, Header = ctx.MultipartTemplateText,
Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.ChapterFileTemplate>(entry.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t)) Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.ChapterFileTemplate>(entry2.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t))
} }
} }
}); });
@ -357,27 +388,26 @@ namespace LibationAvalonia.Views
} }
#endregion #endregion
#region View Bookmarks/Clips (Single book only)
#region View Bookmarks/Clips if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry3 && VisualRoot is Window window)
if (!entry.Liberate.IsSeries && VisualRoot is Window window)
{ {
args.ContextMenuItems.Add(new MenuItem args.ContextMenuItems.Add(new MenuItem
{ {
Header = ctx.ViewBookmarksText, Header = ctx.ViewBookmarksText,
Command = ReactiveCommand.CreateFromTask(() => new BookRecordsDialog(entry.LibraryBook).ShowDialog(window)) Command = ReactiveCommand.CreateFromTask(() => new BookRecordsDialog(entry3.LibraryBook).ShowDialog(window))
}); });
} }
#endregion #endregion
#region View All Series #region View All Series (Single book only)
if (entry.Book.SeriesLink.Any()) if (entries.Length == 1 && entries[0].Book.SeriesLink.Any())
{ {
args.ContextMenuItems.Add(new MenuItem args.ContextMenuItems.Add(new MenuItem
{ {
Header = ctx.ViewSeriesText, Header = ctx.ViewSeriesText,
Command = ReactiveCommand.Create(() => new SeriesViewDialog(entry.LibraryBook).Show()) Command = ReactiveCommand.Create(() => new SeriesViewDialog(entries[0].LibraryBook).Show())
}); });
} }
@ -515,7 +545,7 @@ namespace LibationAvalonia.Views
} }
else if (button.DataContext is ILibraryBookEntry lbEntry) else if (button.DataContext is ILibraryBookEntry lbEntry)
{ {
LiberateClicked?.Invoke(this, lbEntry.LibraryBook); LiberateClicked?.Invoke(this, [lbEntry.LibraryBook]);
} }
} }

View File

@ -5,7 +5,6 @@ using LibationFileManager;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
namespace LibationUiBase.GridView; namespace LibationUiBase.GridView;
@ -17,66 +16,68 @@ public class GridContextMenu
public string SetNotDownloadedText => $"Set Download status to '{Accelerator}Not Downloaded'"; public string SetNotDownloadedText => $"Set Download status to '{Accelerator}Not Downloaded'";
public string RemoveText => $"{Accelerator}Remove from library"; public string RemoveText => $"{Accelerator}Remove from library";
public string LocateFileText => $"{Accelerator}Locate file..."; public string LocateFileText => $"{Accelerator}Locate file...";
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntry.Book.TitleWithSubtitle}'"; public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntries[0].Book.TitleWithSubtitle}'";
public string LocateFileErrorMessage => "Error saving book's location"; public string LocateFileErrorMessage => "Error saving book's location";
public string ConvertToMp3Text => $"{Accelerator}Convert to Mp3"; public string ConvertToMp3Text => $"{Accelerator}Convert to Mp3";
public string ReDownloadText => "Re-download this audiobook"; public string ReDownloadText => "Re-download this audiobook";
public string DownloadSelectedText => "Download selected audiobooks";
public string EditTemplatesText => "Edit Templates"; public string EditTemplatesText => "Edit Templates";
public string FolderTemplateText => "Folder Template"; public string FolderTemplateText => "Folder Template";
public string FileTemplateText => "File Template"; public string FileTemplateText => "File Template";
public string MultipartTemplateText => "Multipart File Template"; public string MultipartTemplateText => "Multipart File Template";
public string ViewBookmarksText => "View _Bookmarks/Clips"; public string ViewBookmarksText => "View _Bookmarks/Clips";
public string ViewSeriesText => GridEntry.Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series"; public string ViewSeriesText => GridEntries[0].Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
public bool LiberateEpisodesEnabled => GridEntry is ISeriesEntry sEntry && sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload); public bool LiberateEpisodesEnabled => GridEntries.OfType<ISeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload));
public bool SetDownloadedEnabled => GridEntry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || GridEntry.Liberate.IsSeries; public bool SetDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || ge.Liberate.IsSeries);
public bool SetNotDownloadedEnabled => GridEntry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || GridEntry.Liberate.IsSeries; public bool SetNotDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || ge.Liberate.IsSeries);
public bool ConvertToMp3Enabled => GridEntry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated; public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
public bool ReDownloadEnabled => GridEntry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated; public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
public IGridEntry GridEntry { get; } private IGridEntry[] GridEntries { get; }
public ILibraryBookEntry[] LibraryBookEntries { get; }
public char Accelerator { get; } public char Accelerator { get; }
public GridContextMenu(IGridEntry gridEntry, char accelerator) public GridContextMenu(IGridEntry[] gridEntries, char accelerator)
{ {
GridEntry = gridEntry; ArgumentNullException.ThrowIfNull(gridEntries, nameof(gridEntries));
ArgumentOutOfRangeException.ThrowIfZero(gridEntries.Length, $"{nameof(gridEntries)}.{nameof(gridEntries.Length)}");
GridEntries = gridEntries;
Accelerator = accelerator; Accelerator = accelerator;
LibraryBookEntries
= GridEntries
.OfType<ISeriesEntry>()
.SelectMany(s => s.Children)
.Concat(GridEntries.OfType<ILibraryBookEntry>())
.ToArray();
} }
public void SetDownloaded() public void SetDownloaded()
{ {
if (GridEntry is ISeriesEntry series) LibraryBookEntries.Select(e => e.LibraryBook)
{ .UpdateUserDefinedItem(udi =>
series.Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated); {
} udi.BookStatus = LiberatedStatus.Liberated;
else if (udi.Book.HasPdf())
{ udi.SetPdfStatus(LiberatedStatus.Liberated);
GridEntry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated); });
}
} }
public void SetNotDownloaded() public void SetNotDownloaded()
{ {
if (GridEntry is ISeriesEntry series) LibraryBookEntries.Select(e => e.LibraryBook)
{ .UpdateUserDefinedItem(udi =>
series.Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated); {
} udi.BookStatus = LiberatedStatus.NotLiberated;
else if (udi.Book.HasPdf())
{ udi.SetPdfStatus(LiberatedStatus.NotLiberated);
GridEntry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated); });
}
} }
public async Task RemoveAsync() public async Task RemoveAsync()
{ {
if (GridEntry is ISeriesEntry series) await LibraryBookEntries.Select(e => e.LibraryBook).RemoveBooksAsync();
{
await series.Children.Select(c => c.LibraryBook).RemoveBooksAsync();
}
else
{
await Task.Run(GridEntry.LibraryBook.RemoveBook);
}
} }
public ITemplateEditor CreateTemplateEditor<T>(LibraryBook libraryBook, string existingTemplate) public ITemplateEditor CreateTemplateEditor<T>(LibraryBook libraryBook, string existingTemplate)

View File

@ -537,9 +537,9 @@
this.productsDisplay.TabIndex = 9; this.productsDisplay.TabIndex = 9;
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged); this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged); this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged);
this.productsDisplay.LiberateClicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_LiberateClicked); this.productsDisplay.LiberateClicked += ProductsDisplay_LiberateClicked;
this.productsDisplay.LiberateSeriesClicked += new System.EventHandler<LibationUiBase.GridView.ISeriesEntry>(this.ProductsDisplay_LiberateSeriesClicked); this.productsDisplay.LiberateSeriesClicked += new System.EventHandler<LibationUiBase.GridView.ISeriesEntry>(this.ProductsDisplay_LiberateSeriesClicked);
this.productsDisplay.ConvertToMp3Clicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_ConvertToMp3Clicked); this.productsDisplay.ConvertToMp3Clicked += ProductsDisplay_ConvertToMp3Clicked;
this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded); this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded);
// //
// toggleQueueHideBtn // toggleQueueHideBtn

View File

@ -23,36 +23,53 @@ namespace LibationWinForms
this.Width = width; this.Width = width;
} }
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook libraryBook) private void ProductsDisplay_LiberateClicked(object sender, LibraryBook[] libraryBooks)
{ {
try try
{ {
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload) if (libraryBooks.Length == 1)
{ {
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", libraryBook); var item = libraryBooks[0];
SetQueueCollapseState(false); if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
processBookQueue1.AddDownloadDecrypt(libraryBook);
}
else if (libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
SetQueueCollapseState(false);
processBookQueue1.AddDownloadPdf(libraryBook);
}
else if (libraryBook.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath?.ShortPathName))
{ {
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}"; Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", item);
MessageBox.Show($"File not found" + suffix); SetQueueCollapseState(false);
processBookQueue1.AddDownloadDecrypt(item);
}
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
SetQueueCollapseState(false);
processBookQueue1.AddDownloadPdf(item);
}
else if (item.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(item.Book.AudibleProductId);
if (!Go.To.File(filePath?.ShortPathName))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
MessageBox.Show($"File not found" + suffix);
}
}
}
else
{
var toLiberate
= libraryBooks
.Where(x => x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
.ToArray();
if (toLiberate.Length > 0)
{
SetQueueCollapseState(false);
processBookQueue1.AddDownloadDecrypt(toLiberate);
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook); Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
} }
} }
@ -72,20 +89,21 @@ namespace LibationWinForms
} }
} }
private void ProductsDisplay_ConvertToMp3Clicked(object sender, LibraryBook libraryBook) private void ProductsDisplay_ConvertToMp3Clicked(object sender, LibraryBook[] libraryBooks)
{ {
try try
{ {
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated) var preLiberated = libraryBooks.Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated).ToArray();
if (preLiberated.Length > 0)
{ {
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook); Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
SetQueueCollapseState(false); SetQueueCollapseState(false);
processBookQueue1.AddConvertMp3(libraryBook); processBookQueue1.AddConvertMp3(preLiberated);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook); Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
} }
} }

View File

@ -20,9 +20,9 @@ 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 EventHandler<int> RemovableCountChanged; public event EventHandler<int> RemovableCountChanged;
public event EventHandler<LibraryBook> LiberateClicked; public event EventHandler<LibraryBook[]> LiberateClicked;
public event EventHandler<ISeriesEntry> LiberateSeriesClicked; public event EventHandler<ISeriesEntry> LiberateSeriesClicked;
public event EventHandler<LibraryBook> ConvertToMp3Clicked; public event EventHandler<LibraryBook[]> ConvertToMp3Clicked;
public event EventHandler InitialLoaded; public event EventHandler InitialLoaded;
private bool hasBeenDisplayed; private bool hasBeenDisplayed;
@ -123,12 +123,12 @@ namespace LibationWinForms.GridView
#region Cell Context Menu #region Cell Context Menu
private void productsGrid_CellContextMenuStripNeeded(IGridEntry entry, ContextMenuStrip ctxMenu) private void productsGrid_CellContextMenuStripNeeded(IGridEntry[] entries, ContextMenuStrip ctxMenu)
{ {
var ctx = new GridContextMenu(entry, '&'); var ctx = new GridContextMenu(entries, '&');
#region Liberate all Episodes #region Liberate all Episodes (Single series only)
if (entry.Liberate.IsSeries) if (entries.Length == 1 && entries[0] is ISeriesEntry seriesEntry)
{ {
var liberateEpisodesMenuItem = new ToolStripMenuItem() var liberateEpisodesMenuItem = new ToolStripMenuItem()
{ {
@ -136,7 +136,7 @@ namespace LibationWinForms.GridView
Enabled = ctx.LiberateEpisodesEnabled Enabled = ctx.LiberateEpisodesEnabled
}; };
liberateEpisodesMenuItem.Click += (_, _) => LiberateSeriesClicked?.Invoke(this, (ISeriesEntry)entry); liberateEpisodesMenuItem.Click += (_, _) => LiberateSeriesClicked?.Invoke(this, seriesEntry);
ctxMenu.Items.Add(liberateEpisodesMenuItem); ctxMenu.Items.Add(liberateEpisodesMenuItem);
} }
@ -163,17 +163,10 @@ namespace LibationWinForms.GridView
ctxMenu.Items.Add(setNotDownloadMenuItem); ctxMenu.Items.Add(setNotDownloadMenuItem);
#endregion #endregion
#region Remove from library #region Locate file (Single book only)
var removeMenuItem = new ToolStripMenuItem() { Text = ctx.RemoveText }; if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry)
removeMenuItem.Click += async (_, _) => await ctx.RemoveAsync();
ctxMenu.Items.Add(removeMenuItem);
#endregion
if (!entry.Liberate.IsSeries)
{ {
#region Locate file
var locateFileMenuItem = new ToolStripMenuItem() { Text = ctx.LocateFileText }; var locateFileMenuItem = new ToolStripMenuItem() { Text = ctx.LocateFileText };
ctxMenu.Items.Add(locateFileMenuItem); ctxMenu.Items.Add(locateFileMenuItem);
locateFileMenuItem.Click += (_, _) => locateFileMenuItem.Click += (_, _) =>
@ -194,23 +187,49 @@ namespace LibationWinForms.GridView
MessageBoxLib.ShowAdminAlert(this, ctx.LocateFileErrorMessage, ctx.LocateFileErrorMessage, ex); MessageBoxLib.ShowAdminAlert(this, ctx.LocateFileErrorMessage, ctx.LocateFileErrorMessage, ex);
} }
}; };
}
#endregion #endregion
#region Convert to Mp3 #region Remove from library
var removeMenuItem = new ToolStripMenuItem() { Text = ctx.RemoveText };
removeMenuItem.Click += async (_, _) => await ctx.RemoveAsync();
ctxMenu.Items.Add(removeMenuItem);
#endregion
#region Liberate All (multiple books only)
if (entries.OfType<ILibraryBookEntry>().Count() > 1)
{
var downloadSelectedMenuItem = new ToolStripMenuItem()
{
Text = ctx.DownloadSelectedText
};
ctxMenu.Items.Add(downloadSelectedMenuItem);
downloadSelectedMenuItem.Click += (s, _) =>
{
LiberateClicked?.Invoke(s, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
};
}
#endregion
#region Convert to Mp3
if (ctx.LibraryBookEntries.Length > 0)
{
var convertToMp3MenuItem = new ToolStripMenuItem var convertToMp3MenuItem = new ToolStripMenuItem
{ {
Text = ctx.ConvertToMp3Text, Text = ctx.ConvertToMp3Text,
Enabled = ctx.ConvertToMp3Enabled Enabled = ctx.ConvertToMp3Enabled
}; };
convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook); convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
ctxMenu.Items.Add(convertToMp3MenuItem); ctxMenu.Items.Add(convertToMp3MenuItem);
#endregion
} }
#region Force Re-Download #endregion
if (!entry.Liberate.IsSeries) #region Force Re-Download (Single book only)
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry4)
{ {
var reDownloadMenuItem = new ToolStripMenuItem() var reDownloadMenuItem = new ToolStripMenuItem()
{ {
@ -220,13 +239,24 @@ namespace LibationWinForms.GridView
ctxMenu.Items.Add(reDownloadMenuItem); ctxMenu.Items.Add(reDownloadMenuItem);
reDownloadMenuItem.Click += (s, _) => reDownloadMenuItem.Click += (s, _) =>
{ {
//No need to persist this change. It only needs to last long for the file to start downloading //No need to persist these changes. They only needs to last long for the files to start downloading
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated; entry4.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
LiberateClicked?.Invoke(s, entry.LibraryBook); if (entry4.Book.HasPdf())
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
LiberateClicked?.Invoke(s, [entry4.LibraryBook]);
}; };
} }
#endregion #endregion
#region Edit Templates
if (entries.Length > 1)
return;
ctxMenu.Items.Add(new ToolStripSeparator());
#region Edit Templates (Single book only)
void editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate) void editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate)
where T : Templates, LibationFileManager.ITemplate, new() where T : Templates, LibationFileManager.ITemplate, new()
{ {
@ -238,14 +268,14 @@ namespace LibationWinForms.GridView
} }
} }
if (!entry.Liberate.IsSeries) if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry2)
{ {
var folderTemplateMenuItem = new ToolStripMenuItem { Text = ctx.FolderTemplateText }; var folderTemplateMenuItem = new ToolStripMenuItem { Text = ctx.FolderTemplateText };
var fileTemplateMenuItem = new ToolStripMenuItem { Text = ctx.FileTemplateText }; var fileTemplateMenuItem = new ToolStripMenuItem { Text = ctx.FileTemplateText };
var multiFileTemplateMenuItem = new ToolStripMenuItem { Text = ctx.MultipartTemplateText }; var multiFileTemplateMenuItem = new ToolStripMenuItem { Text = ctx.MultipartTemplateText };
folderTemplateMenuItem.Click += (s, _) => editTemplate<Templates.FolderTemplate>(entry.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t); folderTemplateMenuItem.Click += (s, _) => editTemplate<Templates.FolderTemplate>(entry2.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t);
fileTemplateMenuItem.Click += (s, _) => editTemplate<Templates.FileTemplate>(entry.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t); fileTemplateMenuItem.Click += (s, _) => editTemplate<Templates.FileTemplate>(entry2.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t);
multiFileTemplateMenuItem.Click += (s, _) => editTemplate<Templates.ChapterFileTemplate>(entry.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t); multiFileTemplateMenuItem.Click += (s, _) => editTemplate<Templates.ChapterFileTemplate>(entry2.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t);
var editTemplatesMenuItem = new ToolStripMenuItem { Text = ctx.EditTemplatesText }; var editTemplatesMenuItem = new ToolStripMenuItem { Text = ctx.EditTemplatesText };
editTemplatesMenuItem.DropDownItems.AddRange(new[] { folderTemplateMenuItem, fileTemplateMenuItem, multiFileTemplateMenuItem }); editTemplatesMenuItem.DropDownItems.AddRange(new[] { folderTemplateMenuItem, fileTemplateMenuItem, multiFileTemplateMenuItem });
@ -255,25 +285,22 @@ namespace LibationWinForms.GridView
} }
#endregion #endregion
#region View Bookmarks/Clips (Single book only)
ctxMenu.Items.Add(new ToolStripSeparator()); if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry3)
#region View Bookmarks/Clips
if (!entry.Liberate.IsSeries)
{ {
var bookRecordMenuItem = new ToolStripMenuItem { Text = ctx.ViewBookmarksText }; var bookRecordMenuItem = new ToolStripMenuItem { Text = ctx.ViewBookmarksText };
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this); bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry3.LibraryBook).ShowDialog(this);
ctxMenu.Items.Add(bookRecordMenuItem); ctxMenu.Items.Add(bookRecordMenuItem);
} }
#endregion #endregion
#region View All Series #region View All Series (Single book only)
if (entry.Book.SeriesLink.Any()) if (entries.Length == 1 && entries[0].Book.SeriesLink.Any())
{ {
var viewSeriesMenuItem = new ToolStripMenuItem { Text = ctx.ViewSeriesText }; var viewSeriesMenuItem = new ToolStripMenuItem { Text = ctx.ViewSeriesText };
viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entry.LibraryBook).Show(); viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entries[0].LibraryBook).Show();
ctxMenu.Items.Add(viewSeriesMenuItem); ctxMenu.Items.Add(viewSeriesMenuItem);
} }
@ -393,7 +420,7 @@ namespace LibationWinForms.GridView
{ {
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error
&& !liveGridEntry.Liberate.IsUnavailable) && !liveGridEntry.Liberate.IsUnavailable)
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook); LiberateClicked?.Invoke(this, [liveGridEntry.LibraryBook]);
} }
private void productsGrid_RemovableCountChanged(object sender, EventArgs e) private void productsGrid_RemovableCountChanged(object sender, EventArgs e)

View File

@ -11,33 +11,34 @@ using System.Drawing;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
#nullable enable
namespace LibationWinForms.GridView namespace LibationWinForms.GridView
{ {
public delegate void GridEntryClickedEventHandler(IGridEntry liveGridEntry); public delegate void GridEntryClickedEventHandler(IGridEntry liveGridEntry);
public delegate void LibraryBookEntryClickedEventHandler(ILibraryBookEntry liveGridEntry); public delegate void LibraryBookEntryClickedEventHandler(ILibraryBookEntry liveGridEntry);
public delegate void GridEntryRectangleClickedEventHandler(IGridEntry liveGridEntry, Rectangle cellRectangle); public delegate void GridEntryRectangleClickedEventHandler(IGridEntry liveGridEntry, Rectangle cellRectangle);
public delegate void ProductsGridCellContextMenuStripNeededEventHandler(IGridEntry liveGridEntry, ContextMenuStrip ctxMenu); public delegate void ProductsGridCellContextMenuStripNeededEventHandler(IGridEntry[] liveGridEntry, ContextMenuStrip ctxMenu);
public partial class ProductsGrid : UserControl public partial class ProductsGrid : UserControl
{ {
/// <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 GridEntryClickedEventHandler CoverClicked; public event GridEntryClickedEventHandler? CoverClicked;
public event LibraryBookEntryClickedEventHandler DetailsClicked; public event LibraryBookEntryClickedEventHandler? DetailsClicked;
public event GridEntryRectangleClickedEventHandler DescriptionClicked; public event GridEntryRectangleClickedEventHandler? DescriptionClicked;
public new event EventHandler<ScrollEventArgs> Scroll; public new event EventHandler<ScrollEventArgs>? Scroll;
public event EventHandler RemovableCountChanged; public event EventHandler? RemovableCountChanged;
public event ProductsGridCellContextMenuStripNeededEventHandler LiberateContextMenuStripNeeded; public event ProductsGridCellContextMenuStripNeededEventHandler? LiberateContextMenuStripNeeded;
private GridEntryBindingList bindingList; private GridEntryBindingList? bindingList;
internal IEnumerable<LibraryBook> GetVisibleBooks() internal IEnumerable<LibraryBook> GetVisibleBooks()
=> bindingList => bindingList
.GetFilteredInItems() ?.GetFilteredInItems()
.Select(lbe => lbe.LibraryBook); .Select(lbe => lbe.LibraryBook) ?? Enumerable.Empty<LibraryBook>();
internal IEnumerable<ILibraryBookEntry> GetAllBookEntries() internal IEnumerable<ILibraryBookEntry> GetAllBookEntries()
=> bindingList.AllItems().BookEntries(); => bindingList?.AllItems().BookEntries() ?? Enumerable.Empty<ILibraryBookEntry>();
public ProductsGrid() public ProductsGrid()
{ {
@ -64,11 +65,17 @@ namespace LibationWinForms.GridView
[PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))] [PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
private void Configuration_FontScaleChanged(object sender, PropertyChangedEventArgsEx e) private void Configuration_FontScaleChanged(object sender, PropertyChangedEventArgsEx e)
=> setGridFontScale((float)e.NewValue); {
if (e.NewValue is float v)
setGridFontScale(v);
}
[PropertyChangeFilter(nameof(Configuration.GridScaleFactor))] [PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
private void Configuration_ScaleChanged(object sender, PropertyChangedEventArgsEx e) private void Configuration_ScaleChanged(object sender, PropertyChangedEventArgsEx e)
=> setGridScale((float)e.NewValue); {
if (e.NewValue is float v)
setGridScale(v);
}
/// <summary> /// <summary>
/// Keep track of the original dimensions for rescaling /// Keep track of the original dimensions for rescaling
@ -106,10 +113,13 @@ namespace LibationWinForms.GridView
#endregion #endregion
private void GridEntryDataGridView_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e) private static string? RemoveLineBreaks(string? text)
=> text?.Replace("\r\n", "").Replace('\r', ' ').Replace('\n', ' ');
private void GridEntryDataGridView_CellContextMenuStripNeeded(object? sender, DataGridViewCellContextMenuStripNeededEventArgs e)
{ {
// header // header
if (e.RowIndex < 0) if (e.RowIndex < 0 || sender is not DataGridView dgv)
return; return;
e.ContextMenuStrip = new ContextMenuStrip(); e.ContextMenuStrip = new ContextMenuStrip();
@ -120,25 +130,89 @@ namespace LibationWinForms.GridView
{ {
try try
{ {
var dgv = (DataGridView)sender; string clipboardText;
var text = dgv[e.ColumnIndex, e.RowIndex].FormattedValue.ToString();
Clipboard.SetDataObject(text, false, 5, 150); if (dgv.SelectedCells.Count <= 1)
{
//Copy contents only of cell that was right-clicked on.
clipboardText = dgv[e.ColumnIndex, e.RowIndex].FormattedValue?.ToString() ?? string.Empty;
}
else
{
//Copy contents of selected cells. Each row is a new line,
//and columns are separated with tabs. Similar formatting to Microsoft Excel.
var selectedCells
= dgv.SelectedCells
.OfType<DataGridViewCell>()
.Where(c => c.OwningColumn is not null && c.OwningRow is not null)
.OrderBy(c => c.RowIndex)
.ThenBy(c => c.OwningColumn!.DisplayIndex)
.ToList();
var headerText
= string.Join("\t",
selectedCells
.Select(c => c.OwningColumn)
.Distinct()
.Select(c => RemoveLineBreaks(c?.HeaderText))
.OfType<string>());
List<string> linesOfText = [headerText];
foreach (var distinctRow in selectedCells.Select(c => c.RowIndex).Distinct())
{
linesOfText.Add(string.Join("\t",
selectedCells
.Where(c => c.RowIndex == distinctRow)
.Select(c => RemoveLineBreaks(c.FormattedValue?.ToString()) ?? string.Empty)
));
}
clipboardText = string.Join(Environment.NewLine, linesOfText);
}
Clipboard.SetDataObject(clipboardText, false, 5, 150);
}
catch(Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error copying text to clipboard");
} }
catch { }
}); });
e.ContextMenuStrip.Items.Add(new ToolStripSeparator()); e.ContextMenuStrip.Items.Add(new ToolStripSeparator());
} }
var entry = getGridEntry(e.RowIndex); var clickedEntry = getGridEntry(e.RowIndex);
var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip); var allSelected
= gridEntryDataGridView
.SelectedCells
.OfType<DataGridViewCell>()
.Select(c => c.OwningRow)
.OfType<DataGridViewRow>()
.Distinct()
.OrderBy(r => r.Index)
.Select(r => r.DataBoundItem)
.OfType<IGridEntry>()
.ToArray();
var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
if (clickedIndex == -1)
{
//User didn't right-click on a selected cell
gridEntryDataGridView.ClearSelection();
gridEntryDataGridView[e.ColumnIndex, e.RowIndex].Selected = true;
allSelected = [clickedEntry];
}
else if (clickedIndex > 0)
{
//Ensure the clicked entry is first in the list
(allSelected[0], allSelected[clickedIndex]) = (allSelected[clickedIndex], allSelected[0]);
}
LiberateContextMenuStripNeeded?.Invoke(allSelected, e.ContextMenuStrip);
} }
private void EnableDoubleBuffering() private void EnableDoubleBuffering()
{ {
var propertyInfo = gridEntryDataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var propertyInfo = gridEntryDataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
propertyInfo.SetValue(gridEntryDataGridView, true, null); propertyInfo?.SetValue(gridEntryDataGridView, true, null);
} }
#region Button controls #region Button controls
@ -167,11 +241,11 @@ namespace LibationWinForms.GridView
if (e.ColumnIndex == liberateGVColumn.Index) if (e.ColumnIndex == liberateGVColumn.Index)
{ {
if (sEntry.Liberate.Expanded) if (sEntry.Liberate.Expanded)
bindingList.CollapseItem(sEntry); bindingList?.CollapseItem(sEntry);
else else
bindingList.ExpandItem(sEntry); bindingList?.ExpandItem(sEntry);
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count()); VisibleCountChanged?.Invoke(this, bindingList?.GetFilteredInItems().Count() ?? 0);
} }
else if (e.ColumnIndex == descriptionGVColumn.Index) else if (e.ColumnIndex == descriptionGVColumn.Index)
DescriptionClicked?.Invoke(sEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false)); DescriptionClicked?.Invoke(sEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
@ -202,7 +276,7 @@ namespace LibationWinForms.GridView
get => removeGVColumn.Visible; get => removeGVColumn.Visible;
set set
{ {
if (value) if (value && bindingList is not null)
{ {
foreach (var book in bindingList.AllItems()) foreach (var book in bindingList.AllItems())
book.Remove = false; book.Remove = false;
@ -248,13 +322,16 @@ namespace LibationWinForms.GridView
internal void UpdateGrid(List<LibraryBook> dbBooks) internal void UpdateGrid(List<LibraryBook> dbBooks)
{ {
if (bindingList == null)
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(UpdateGrid)}");
//First row that is in view in the DataGridView //First row that is in view in the DataGridView
var topRow = gridEntryDataGridView.Rows.Cast<DataGridViewRow>().FirstOrDefault(r => r.Displayed)?.Index ?? 0; var topRow = gridEntryDataGridView.Rows.Cast<DataGridViewRow>().FirstOrDefault(r => r.Displayed)?.Index ?? 0;
#region Add new or update existing grid entries #region Add new or update existing grid entries
//Remove filter prior to adding/updating boooks //Remove filter prior to adding/updating books
string existingFilter = syncBindingSource.Filter; string? existingFilter = syncBindingSource.Filter;
Filter(null); Filter(null);
//Add absent entries to grid, or update existing entry //Add absent entries to grid, or update existing entry
@ -308,6 +385,9 @@ namespace LibationWinForms.GridView
public void RemoveBooks(IEnumerable<ILibraryBookEntry> removedBooks) public void RemoveBooks(IEnumerable<ILibraryBookEntry> removedBooks)
{ {
if (bindingList == null)
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(RemoveBooks)}");
//Remove books in series from their parents' Children list //Remove books in series from their parents' Children list
foreach (var removed in removedBooks.Where(b => b.Liberate.IsEpisode)) foreach (var removed in removedBooks.Where(b => b.Liberate.IsEpisode))
removed.Parent.RemoveChild(removed); removed.Parent.RemoveChild(removed);
@ -325,8 +405,11 @@ namespace LibationWinForms.GridView
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count()); VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
} }
private void AddOrUpdateBook(LibraryBook book, ILibraryBookEntry existingBookEntry) private void AddOrUpdateBook(LibraryBook book, ILibraryBookEntry? existingBookEntry)
{ {
if (bindingList == null)
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(AddOrUpdateBook)}");
if (existingBookEntry is null) if (existingBookEntry is null)
// Add the new product to top // Add the new product to top
bindingList.Insert(0, new LibraryBookEntry<WinFormsEntryStatus>(book)); bindingList.Insert(0, new LibraryBookEntry<WinFormsEntryStatus>(book));
@ -335,8 +418,11 @@ namespace LibationWinForms.GridView
existingBookEntry.UpdateLibraryBook(book); existingBookEntry.UpdateLibraryBook(book);
} }
private void AddOrUpdateEpisode(LibraryBook episodeBook, ILibraryBookEntry existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks) private void AddOrUpdateEpisode(LibraryBook episodeBook, ILibraryBookEntry? existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
{ {
if (bindingList == null)
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(AddOrUpdateEpisode)}");
if (existingEpisodeEntry is null) if (existingEpisodeEntry is null)
{ {
ILibraryBookEntry episodeEntry; ILibraryBookEntry episodeEntry;
@ -397,7 +483,7 @@ namespace LibationWinForms.GridView
#region Filter #region Filter
public void Filter(string searchString) public void Filter(string? searchString)
{ {
if (bindingList is null) return; if (bindingList is null) return;
@ -485,16 +571,16 @@ namespace LibationWinForms.GridView
removeGVColumn.IndeterminateValue = null; removeGVColumn.IndeterminateValue = null;
} }
private void HideMenuItem_Click(object sender, EventArgs e) private void HideMenuItem_Click(object? sender, EventArgs e)
{ {
var menuItem = sender as ToolStripMenuItem; var menuItem = sender as ToolStripMenuItem;
var propertyName = menuItem.Tag as string; var propertyName = menuItem?.Tag as string;
var column = gridEntryDataGridView.Columns var column = gridEntryDataGridView.Columns
.Cast<DataGridViewColumn>() .Cast<DataGridViewColumn>()
.FirstOrDefault(c => c.DataPropertyName == propertyName); .FirstOrDefault(c => c.DataPropertyName == propertyName);
if (column != null) if (column != null && menuItem != null && propertyName != null)
{ {
var visible = menuItem.Checked; var visible = menuItem.Checked;
menuItem.Checked = !visible; menuItem.Checked = !visible;
@ -508,7 +594,7 @@ namespace LibationWinForms.GridView
} }
} }
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e) private void gridEntryDataGridView_ColumnDisplayIndexChanged(object? sender, DataGridViewColumnEventArgs e)
{ {
var config = Configuration.Instance; var config = Configuration.Instance;
@ -517,7 +603,7 @@ namespace LibationWinForms.GridView
config.GridColumnsDisplayIndices = dictionary; config.GridColumnsDisplayIndices = dictionary;
} }
private void gridEntryDataGridView_CellToolTipTextNeeded(object sender, DataGridViewCellToolTipTextNeededEventArgs e) private void gridEntryDataGridView_CellToolTipTextNeeded(object? sender, DataGridViewCellToolTipTextNeededEventArgs e)
{ {
if (e.ColumnIndex == descriptionGVColumn.Index) if (e.ColumnIndex == descriptionGVColumn.Index)
e.ToolTipText = "Click to see full description"; e.ToolTipText = "Click to see full description";
@ -525,7 +611,7 @@ namespace LibationWinForms.GridView
e.ToolTipText = "Click to see full size"; e.ToolTipText = "Click to see full size";
} }
private void gridEntryDataGridView_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e) private void gridEntryDataGridView_ColumnWidthChanged(object? sender, DataGridViewColumnEventArgs e)
{ {
var config = Configuration.Instance; var config = Configuration.Instance;