diff --git a/Source/ApplicationServices/ISearchEngine.cs b/Source/ApplicationServices/ISearchEngine.cs
new file mode 100644
index 00000000..d7e171e9
--- /dev/null
+++ b/Source/ApplicationServices/ISearchEngine.cs
@@ -0,0 +1,9 @@
+using LibationSearchEngine;
+
+#nullable enable
+namespace ApplicationServices;
+
+public interface ISearchEngine
+{
+ SearchResultSet? GetSearchResultSet(string? searchString);
+}
diff --git a/Source/ApplicationServices/MainSearchEngine.cs b/Source/ApplicationServices/MainSearchEngine.cs
new file mode 100644
index 00000000..52783801
--- /dev/null
+++ b/Source/ApplicationServices/MainSearchEngine.cs
@@ -0,0 +1,16 @@
+using LibationSearchEngine;
+
+#nullable enable
+namespace ApplicationServices;
+
+///
+/// The main search engine used Libation.
+/// Acts as an adapter to SearchEngineCommands.Search()
+///
+public class MainSearchEngine : ISearchEngine
+{
+ public static MainSearchEngine Instance { get; } = new MainSearchEngine();
+ private MainSearchEngine() { }
+ public SearchResultSet? GetSearchResultSet(string? searchString)
+ => string.IsNullOrEmpty(searchString) ? null : SearchEngineCommands.Search(searchString);
+}
diff --git a/Source/ApplicationServices/TempSearchEngine.cs b/Source/ApplicationServices/TempSearchEngine.cs
new file mode 100644
index 00000000..527f67fe
--- /dev/null
+++ b/Source/ApplicationServices/TempSearchEngine.cs
@@ -0,0 +1,45 @@
+using DataLayer;
+using LibationFileManager;
+using LibationSearchEngine;
+using System.Collections.Generic;
+
+#nullable enable
+namespace ApplicationServices;
+
+///
+/// A temporary search engine created in InProgress/TempSearchEngine
+/// Used for Trash Bin searches to avoid interfering with the main search engine
+///
+public class TempSearchEngine : ISearchEngine
+{
+ public static string SearchEnginePath { get; }
+ = System.IO.Path.Combine(Configuration.Instance.InProgress, nameof(TempSearchEngine));
+ private SearchEngine SearchEngine { get; } = new SearchEngine(SearchEnginePath);
+
+ public bool ReindexSearchEngine(IEnumerable books)
+ {
+ try
+ {
+ SearchEngine.CreateNewIndex(books, overwrite: true);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ public SearchResultSet? GetSearchResultSet(string? searchString)
+ {
+ if (string.IsNullOrEmpty(searchString))
+ return null;
+
+ try
+ {
+ return SearchEngine.Search(searchString);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs
index 5a2275ab..7e710666 100644
--- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs
+++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs
@@ -47,7 +47,8 @@ namespace DataLayer
=> context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
- .Where(lb => lb.IsDeleted)
+ //Return all parents so the trash bin grid can show podcasts beneath their parents
+ .Where(lb => lb.IsDeleted || lb.Book.ContentType == ContentType.Parent)
.getLibrary()
.ToList();
diff --git a/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs b/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs
index da2ea8fa..009e67ea 100644
--- a/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs
+++ b/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs
@@ -97,7 +97,7 @@ public class TrashBinViewModel : ViewModelBase, IDisposable
public void Reload()
{
- var deletedBooks = DbContexts.GetDeletedLibraryBooks();
+ var deletedBooks = DbContexts.GetDeletedLibraryBooks().Where(lb => lb.Book.ContentType is not ContentType.Parent);
DeletedBooks.Clear();
DeletedBooks.AddRange(deletedBooks.Select(lb => new CheckBoxViewModel { Item = lb }));
diff --git a/Source/HangoverWinForms/Form1.Deleted.cs b/Source/HangoverWinForms/Form1.Deleted.cs
index e48bd511..40d6fef1 100644
--- a/Source/HangoverWinForms/Form1.Deleted.cs
+++ b/Source/HangoverWinForms/Form1.Deleted.cs
@@ -54,7 +54,7 @@ namespace HangoverWinForms
deletedCbl.Items.Clear();
List deletedBooks = DbContexts.GetDeletedLibraryBooks();
- foreach (var lb in deletedBooks)
+ foreach (var lb in deletedBooks.Where(lb => lb.Book.ContentType is not ContentType.Parent))
deletedCbl.Items.Add(lb);
setLabel();
diff --git a/Source/LibationAvalonia/Controls/DataGridContextMenus.cs b/Source/LibationAvalonia/Controls/DataGridContextMenus.cs
index 4e0a7b2d..2bddde98 100644
--- a/Source/LibationAvalonia/Controls/DataGridContextMenus.cs
+++ b/Source/LibationAvalonia/Controls/DataGridContextMenus.cs
@@ -1,119 +1,121 @@
using Avalonia.Collections;
using Avalonia.Controls;
-using LibationUiBase.GridView;
+using Avalonia.Input;
using System;
+using System.Collections.Generic;
using System.Linq;
-using System.Reflection;
-namespace LibationAvalonia.Controls
+namespace LibationAvalonia.Controls;
+
+public class DataGridCellContextMenu where TContext : class
{
- internal static class DataGridContextMenus
+ public static DataGridCellContextMenu? Create(ContextMenu? contextMenu)
{
- public static event EventHandler? CellContextMenuStripNeeded;
- private static readonly ContextMenu ContextMenu = new();
- public static readonly AvaloniaList MenuItems = new();
- private static readonly PropertyInfo OwningColumnProperty;
- private static readonly PropertyInfo OwningGridProperty;
-
- static DataGridContextMenus()
+ DataGrid? grid = null;
+ DataGridCell? cell = null;
+ var parent = contextMenu?.Parent;
+ while (parent is not null && grid is null)
{
- ContextMenu.ItemsSource = MenuItems;
- OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic)
- ?? throw new InvalidOperationException("Could not find OwningColumn property on DataGridCell");
- OwningGridProperty = typeof(DataGridColumn).GetProperty("OwningGrid", BindingFlags.Instance | BindingFlags.NonPublic)
- ?? throw new InvalidOperationException("Could not find OwningGrid property on DataGridColumn");
+ grid ??= parent as DataGrid;
+ cell ??= parent as DataGridCell;
+
+ parent = parent.Parent;
}
- public static void AttachContextMenu(this DataGridCell cell)
+ if (grid is null || cell is null || cell.Tag is not DataGridColumn column || contextMenu!.DataContext is not TContext clickedEntry)
+ return null;
+
+ var allSelected = grid.SelectedItems.OfType().ToArray();
+ var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
+ if (clickedIndex == -1)
{
- if (cell is not null && cell.ContextMenu is null)
- {
- cell.ContextRequested += Cell_ContextRequested;
- cell.ContextMenu = ContextMenu;
- }
+ //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]);
}
- private static void Cell_ContextRequested(object? sender, ContextRequestedEventArgs e)
+ return new DataGridCellContextMenu(contextMenu, grid, column, allSelected);
+ }
+
+ public string CellClipboardContents
+ {
+ get
{
- if (sender is DataGridCell cell &&
- cell.DataContext is GridEntry clickedEntry &&
- OwningColumnProperty.GetValue(cell) is DataGridColumn column &&
- OwningGridProperty.GetValue(column) is DataGrid grid)
+ var lines = GetClipboardLines(getClickedCell: true);
+ return lines.Count >= 1 ? lines[0] : string.Empty;
+ }
+ }
+ public string GetRowClipboardContents() => string.Join(Environment.NewLine, GetClipboardLines(false));
+
+ public ContextMenu ContextMenu { get; }
+ public DataGrid Grid { get; }
+ public DataGridColumn Column { get; }
+ public TContext[] RowItems { get; }
+ public AvaloniaList ContextMenuItems { get; }
+
+ private DataGridCellContextMenu(ContextMenu contextMenu, DataGrid grid, DataGridColumn column, TContext[] rowItems)
+ {
+ Grid = grid;
+ Column = column;
+ RowItems = rowItems;
+ ContextMenu = contextMenu;
+ ContextMenuItems = contextMenu.ItemsSource as AvaloniaList ?? new();
+ contextMenu.ItemsSource = ContextMenuItems;
+ ContextMenuItems.Clear();
+ }
+
+ private List GetClipboardLines(bool getClickedCell)
+ {
+ if (RowItems is null || RowItems.Length == 0)
+ return [];
+
+ List lines = [];
+ Grid.CopyingRowClipboardContent += Grid_CopyingRowClipboardContent;
+ Grid.RaiseEvent(GetCopyEventArgs());
+ Grid.CopyingRowClipboardContent -= Grid_CopyingRowClipboardContent;
+ return lines;
+
+ void Grid_CopyingRowClipboardContent(object? sender, DataGridRowClipboardEventArgs e)
+ {
+ if (getClickedCell)
{
- var allSelected = grid.SelectedItems.OfType().ToArray();
- var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
- if (clickedIndex == -1)
+ if (e.IsColumnHeadersRow)
+ return;
+ var cellContent = e.ClipboardRowContent.FirstOrDefault(c => c.Column == Column);
+ if (cellContent.Column is not null)
{
- //User didn't right-click on a selected cell
- grid.SelectedItem = clickedEntry;
- allSelected = [clickedEntry];
+ lines.Add(cellContent.Content?.ToString() ?? string.Empty);
}
- 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
- {
- Column = column,
- Grid = grid,
- GridEntries = allSelected,
- ContextMenu = ContextMenu
- };
-
- args.ContextMenuItems.Clear();
- CellContextMenuStripNeeded?.Invoke(sender, args);
- e.Handled = args.ContextMenuItems.Count == 0;
}
+ else if (e.Item == RowItems[0])
+ lines.Insert(1, FormatClipboardRowContent(e));
else
- e.Handled = true;
+ lines.Add(FormatClipboardRowContent(e));
+
+ //Clear so that the DataGrid copy implementation doesn't set the clipboard
+ e.ClipboardRowContent.Clear();
}
}
- public class DataGridCellContextMenuStripNeededEventArgs
+ private static KeyEventArgs GetCopyEventArgs() => new()
{
- private static readonly MethodInfo GetCellValueMethod;
- static DataGridCellContextMenuStripNeededEventArgs()
- {
- GetCellValueMethod = typeof(DataGridColumn).GetMethod("GetCellValue", BindingFlags.NonPublic | BindingFlags.Instance)
- ?? throw new InvalidOperationException("Could not find GetCellValue method on DataGridColumn");
- }
+ Key = Key.C,
+ KeyModifiers = KeyModifiers.Control,
+ Route = Avalonia.Interactivity.RoutingStrategies.Bubble,
+ PhysicalKey = PhysicalKey.C,
+ KeySymbol = "c",
+ KeyDeviceType = KeyDeviceType.Keyboard,
+ RoutedEvent = InputElement.KeyDownEvent
+ };
- private static string GetCellValue(DataGridColumn column, object item)
- => GetCellValueMethod.Invoke(column, new object[] { item, column.ClipboardContentBinding })?.ToString() ?? "";
+ private string FormatClipboardRowContent(DataGridRowClipboardEventArgs e)
+ => string.Join("\t", e.ClipboardRowContent.Select(c => RemoveLineBreaks(c.Content?.ToString())));
+ private static string RemoveLineBreaks(string? text)
+ => text?.Replace("\r\n", " ").Replace('\r', ' ').Replace('\n', ' ') ?? "";
- public string CellClipboardContents => GetCellValue(Column, GridEntries[0]);
- public string GetRowClipboardContents()
- {
- 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(GridEntry 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 GridEntry[] GridEntries { get; init; }
- public required ContextMenu ContextMenu { get; init; }
- public AvaloniaList ContextMenuItems => DataGridContextMenus.MenuItems;
- }
}
diff --git a/Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs b/Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs
index 8ff392e7..5062ccd1 100644
--- a/Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs
+++ b/Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs
@@ -25,7 +25,7 @@ namespace LibationAvalonia.Controls
IsEditingMode = false
};
- cell?.AttachContextMenu();
+ cell.Tag = this;
if (!IsReadOnly)
ToolTip.SetTip(myRatingElement, "Click to change ratings");
diff --git a/Source/LibationAvalonia/Controls/DataGridTemplateColumnExt.cs b/Source/LibationAvalonia/Controls/DataGridTemplateColumnExt.cs
index c65358aa..cda02234 100644
--- a/Source/LibationAvalonia/Controls/DataGridTemplateColumnExt.cs
+++ b/Source/LibationAvalonia/Controls/DataGridTemplateColumnExt.cs
@@ -6,7 +6,7 @@ namespace LibationAvalonia.Controls
{
protected override Control GenerateElement(DataGridCell cell, object dataItem)
{
- cell?.AttachContextMenu();
+ cell.Tag = this;
return base.GenerateElement(cell, dataItem);
}
}
diff --git a/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml b/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml
index 34f200b4..891504a5 100644
--- a/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml
@@ -4,49 +4,72 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="630" d:DesignHeight="480"
x:Class="LibationAvalonia.Dialogs.TrashBinDialog"
- xmlns:controls="clr-namespace:LibationAvalonia.Controls"
- MinWidth="630" MinHeight="480"
- Width="630" Height="480"
+ xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
+ xmlns:views="clr-namespace:LibationAvalonia.Views"
+ x:DataType="dialogs:TrashBinViewModel"
+ x:CompileBindings="True"
+ MinWidth="680" MinHeight="480"
+ Width="680" Height="480"
Title="Trash Bin"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+ Margin="0,5,0,0"
+ Grid.Row="3"
+ ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto">
+
+
+
+