mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-02-18 00:17:43 +01:00
Improve TrashBinDialog functionality
- Use the main display grid control to display deleted books - Added search functionality for deleted books. This required creating a temporary search index in the `InProgress` folder. The products grid control now uses an instance of `ISearchEngine` to filter its grid entries. The main grid uses a singleton instance of `MainSearchEngine`, which merely wraps `SearchEngineCommands.Search()`. The TrashBinDialogs use `TempSearchEngine`. - Users can now batch select `Everyting` as well as `Audible Plus Books` Avalonia: - Refactor main grid context menus to no longer require reflection
This commit is contained in:
9
Source/ApplicationServices/ISearchEngine.cs
Normal file
9
Source/ApplicationServices/ISearchEngine.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using LibationSearchEngine;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices;
|
||||
|
||||
public interface ISearchEngine
|
||||
{
|
||||
SearchResultSet? GetSearchResultSet(string? searchString);
|
||||
}
|
||||
16
Source/ApplicationServices/MainSearchEngine.cs
Normal file
16
Source/ApplicationServices/MainSearchEngine.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using LibationSearchEngine;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices;
|
||||
|
||||
/// <summary>
|
||||
/// The main search engine used Libation.
|
||||
/// Acts as an adapter to SearchEngineCommands.Search()
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
45
Source/ApplicationServices/TempSearchEngine.cs
Normal file
45
Source/ApplicationServices/TempSearchEngine.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationSearchEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices;
|
||||
|
||||
/// <summary>
|
||||
/// A temporary search engine created in InProgress/TempSearchEngine
|
||||
/// Used for Trash Bin searches to avoid interfering with the main search engine
|
||||
/// </summary>
|
||||
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<LibraryBook> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace HangoverWinForms
|
||||
deletedCbl.Items.Clear();
|
||||
List<LibraryBook> 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();
|
||||
|
||||
@@ -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<TContext> where TContext : class
|
||||
{
|
||||
internal static class DataGridContextMenus
|
||||
public static DataGridCellContextMenu<TContext>? Create(ContextMenu? contextMenu)
|
||||
{
|
||||
public static event EventHandler<DataGridCellContextMenuStripNeededEventArgs>? CellContextMenuStripNeeded;
|
||||
private static readonly ContextMenu ContextMenu = new();
|
||||
public static readonly AvaloniaList<Control> 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<TContext>().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<TContext>(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<Control> 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<Control> ?? new();
|
||||
contextMenu.ItemsSource = ContextMenuItems;
|
||||
ContextMenuItems.Clear();
|
||||
}
|
||||
|
||||
private List<string> GetClipboardLines(bool getClickedCell)
|
||||
{
|
||||
if (RowItems is null || RowItems.Length == 0)
|
||||
return [];
|
||||
|
||||
List<string> 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<GridEntry>().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<Control> ContextMenuItems => DataGridContextMenus.MenuItems;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace LibationAvalonia.Controls
|
||||
IsEditingMode = false
|
||||
};
|
||||
|
||||
cell?.AttachContextMenu();
|
||||
cell.Tag = this;
|
||||
|
||||
if (!IsReadOnly)
|
||||
ToolTip.SetTip(myRatingElement, "Click to change ratings");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
<Grid
|
||||
RowDefinitions="Auto,*,Auto">
|
||||
<Grid Margin="5"
|
||||
RowDefinitions="Auto,Auto,*,Auto">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
Text="Check books you want to permanently delete from or restore to Libation" />
|
||||
<TextBlock Text="Check books you want to permanently delete from or restore to Libation"/>
|
||||
|
||||
<controls:CheckedListBox
|
||||
Grid.Row="1"
|
||||
Margin="5,0,5,0"
|
||||
BorderThickness="1"
|
||||
BorderBrush="Gray"
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
Items="{Binding DeletedBooks}" />
|
||||
<Grid Margin="0,5" Grid.Row="1" Grid.ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock VerticalAlignment="Center" Text="Search Deleted Books:"/>
|
||||
<TextBox Name="searchTb" Margin="5,0" Grid.Column="1" Text="{Binding FilterString}" IsEnabled="{Binding ControlsEnabled}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Command="{Binding FilterBtnAsync}" Gesture="Enter" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Classes="SaveButton" Grid.Column="2" Command="{Binding FilterBtnAsync}" IsEnabled="{Binding ControlsEnabled}" VerticalAlignment="Stretch" Content="Filter" />
|
||||
</Grid>
|
||||
|
||||
<views:ProductsDisplay
|
||||
Grid.Row="2"
|
||||
DisableContextMenu="True"
|
||||
DisableColumnCustomization="True"
|
||||
IsEnabled="{Binding $parent.((dialogs:TrashBinViewModel)DataContext).ControlsEnabled}"
|
||||
DataContext="{Binding ProductsDisplay}" />
|
||||
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
Margin="0,5,0,0"
|
||||
Grid.Row="3"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto">
|
||||
|
||||
<CheckBox
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
IsThreeState="True"
|
||||
Margin="0,0,20,0"
|
||||
Margin="0,0,14,0"
|
||||
IsChecked="{Binding EverythingChecked}"
|
||||
Content="Everything" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="0,0,14,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding CheckedCountText}" />
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="2"
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
IsThreeState="True"
|
||||
Margin="0,0,15,0"
|
||||
IsChecked="{Binding AudiblePlusChecked}"
|
||||
Content="Audible Plus Books" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding AudiblePlusCheckedCountText}" />
|
||||
|
||||
<Button
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
Grid.Column="2"
|
||||
Grid.Column="4"
|
||||
Margin="0,0,20,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
@@ -56,7 +79,7 @@
|
||||
|
||||
<Button
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
Grid.Column="3"
|
||||
Grid.Column="5"
|
||||
Command="{Binding PermanentlyDeleteCheckedAsync}" >
|
||||
<TextBlock
|
||||
TextAlignment="Center"
|
||||
|
||||
@@ -1,140 +1,159 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.Controls;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
namespace LibationAvalonia.Dialogs;
|
||||
|
||||
public partial class TrashBinDialog : DialogWindow
|
||||
{
|
||||
public partial class TrashBinDialog : DialogWindow
|
||||
private TrashBinViewModel VM { get; }
|
||||
public TrashBinDialog()
|
||||
{
|
||||
public TrashBinDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new TrashBinViewModel();
|
||||
InitializeComponent();
|
||||
SaveOnEnter = false;
|
||||
ControlToFocusOnShow = searchTb;
|
||||
DataContext = VM = new TrashBinViewModel();
|
||||
|
||||
this.Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
|
||||
KeyBindings.Add(new Avalonia.Input.KeyBinding
|
||||
{
|
||||
Gesture = new Avalonia.Input.KeyGesture(Avalonia.Input.Key.Escape),
|
||||
Command = ReactiveCommand.Create(Close)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class TrashBinViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
public AvaloniaList<CheckBoxViewModel> DeletedBooks { get; }
|
||||
public string CheckedCountText => $"Checked : {_checkedBooksCount} of {_totalBooksCount}";
|
||||
public bool ControlsEnabled { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = true;
|
||||
|
||||
private bool? everythingChecked = false;
|
||||
public bool? EverythingChecked
|
||||
{
|
||||
get => everythingChecked;
|
||||
set
|
||||
{
|
||||
everythingChecked = value ?? false;
|
||||
|
||||
if (everythingChecked is true)
|
||||
CheckAll();
|
||||
else if (everythingChecked is false)
|
||||
UncheckAll();
|
||||
}
|
||||
}
|
||||
|
||||
private int _totalBooksCount = 0;
|
||||
private int _checkedBooksCount = -1;
|
||||
public int CheckedBooksCount
|
||||
{
|
||||
get => _checkedBooksCount;
|
||||
set
|
||||
{
|
||||
if (_checkedBooksCount != value)
|
||||
{
|
||||
_checkedBooksCount = value;
|
||||
this.RaisePropertyChanged(nameof(CheckedCountText));
|
||||
}
|
||||
|
||||
everythingChecked
|
||||
= _checkedBooksCount == 0 || _totalBooksCount == 0 ? false
|
||||
: _checkedBooksCount == _totalBooksCount ? true
|
||||
: null;
|
||||
|
||||
this.RaisePropertyChanged(nameof(EverythingChecked));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<LibraryBook> CheckedBooks => DeletedBooks.Where(i => i.IsChecked).Select(i => i.Item).Cast<LibraryBook>();
|
||||
|
||||
public TrashBinViewModel()
|
||||
{
|
||||
DeletedBooks = new()
|
||||
{
|
||||
ResetBehavior = ResetBehavior.Remove
|
||||
};
|
||||
|
||||
tracker = DeletedBooks.TrackItemPropertyChanged(CheckboxPropertyChanged);
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void CheckAll()
|
||||
{
|
||||
foreach (var item in DeletedBooks)
|
||||
item.IsChecked = true;
|
||||
}
|
||||
|
||||
public void UncheckAll()
|
||||
{
|
||||
foreach (var item in DeletedBooks)
|
||||
item.IsChecked = false;
|
||||
}
|
||||
|
||||
public async Task RestoreCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await CheckedBooks.RestoreBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
public async Task PermanentlyDeleteCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await CheckedBooks.PermanentlyDeleteBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
|
||||
DeletedBooks.Clear();
|
||||
DeletedBooks.AddRange(deletedBooks.Select(lb => new CheckBoxViewModel { Item = lb }));
|
||||
|
||||
_totalBooksCount = DeletedBooks.Count;
|
||||
CheckedBooksCount = 0;
|
||||
}
|
||||
|
||||
private IDisposable tracker;
|
||||
private void CheckboxPropertyChanged(Tuple<object?, PropertyChangedEventArgs> e)
|
||||
{
|
||||
if (e.Item2.PropertyName == nameof(CheckBoxViewModel.IsChecked))
|
||||
CheckedBooksCount = DeletedBooks.Count(b => b.IsChecked);
|
||||
}
|
||||
|
||||
public void Dispose() => tracker?.Dispose();
|
||||
Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
Loaded += async (_, _) => await VM.InitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class TrashBinViewModel : ViewModelBase
|
||||
{
|
||||
private TempSearchEngine SearchEngine { get; } = new();
|
||||
public ProductsDisplayViewModel ProductsDisplay { get; }
|
||||
public string? CheckedCountText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? AudiblePlusCheckedCountText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool ControlsEnabled { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? FilterString { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
private bool? m_everythingChecked = false;
|
||||
private bool? m_audiblePlusChecked = false;
|
||||
public bool? EverythingChecked
|
||||
{
|
||||
get => m_everythingChecked;
|
||||
set
|
||||
{
|
||||
m_everythingChecked = value ?? false;
|
||||
SetVisibleChecked(_ => true, m_everythingChecked.Value);
|
||||
}
|
||||
}
|
||||
public bool? AudiblePlusChecked
|
||||
{
|
||||
get => m_audiblePlusChecked;
|
||||
set
|
||||
{
|
||||
m_audiblePlusChecked = value ?? false;
|
||||
SetVisibleChecked(e => e.IsAudiblePlus, m_audiblePlusChecked.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public TrashBinViewModel()
|
||||
{
|
||||
ProductsDisplay = new() { SearchEngine = SearchEngine };
|
||||
ProductsDisplay.RemovableCountChanged += (_, _) => UpdateCounts();
|
||||
ProductsDisplay.VisibleCountChanged += (_, _) => UpdateCounts();
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var deletedBooks = GetDeletedLibraryBooks();
|
||||
SearchEngine.ReindexSearchEngine(deletedBooks);
|
||||
await ProductsDisplay.BindToGridAsync(deletedBooks);
|
||||
await ProductsDisplay.ScanAndRemoveBooksAsync();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
var deletedBooks = GetDeletedLibraryBooks();
|
||||
SearchEngine.ReindexSearchEngine(deletedBooks);
|
||||
await ProductsDisplay.UpdateGridAsync(deletedBooks);
|
||||
}
|
||||
|
||||
public void SetVisibleChecked(Func<LibraryBook, bool> predicate, bool isChecked)
|
||||
{
|
||||
ProductsDisplay.GetVisibleGridEntries().Where(e => predicate(e.LibraryBook)).ForEach(i => i.Remove = isChecked);
|
||||
}
|
||||
|
||||
private IEnumerable<LibraryBook> GetCheckedBooks() => ProductsDisplay.GetVisibleGridEntries().Where(i => i.Remove is true).Select(i => i.LibraryBook);
|
||||
|
||||
private void UpdateCounts()
|
||||
{
|
||||
var visible = ProductsDisplay.GetVisibleGridEntries().ToArray();
|
||||
var plusVisibleCount = visible.Count(e => e.LibraryBook.IsAudiblePlus);
|
||||
|
||||
var checkedCount = visible.Count(e => e.Remove is true);
|
||||
var plusCheckedCount = visible.Count(e => e.LibraryBook.IsAudiblePlus && e.Remove is true);
|
||||
|
||||
CheckedCountText = $"Checked: {checkedCount} of {visible.Length}";
|
||||
AudiblePlusCheckedCountText = $"Checked: {plusCheckedCount} of {plusVisibleCount}";
|
||||
|
||||
bool? everythingChecked = checkedCount == 0 || visible.Length == 0 ? false
|
||||
: checkedCount == visible.Length ? true
|
||||
: null;
|
||||
|
||||
bool? audiblePlusChecked = plusCheckedCount == 0 || plusVisibleCount == 0 ? false
|
||||
: plusCheckedCount == plusVisibleCount ? true
|
||||
: null;
|
||||
|
||||
this.RaiseAndSetIfChanged(ref m_everythingChecked, everythingChecked, nameof(EverythingChecked));
|
||||
this.RaiseAndSetIfChanged(ref m_audiblePlusChecked, audiblePlusChecked, nameof(AudiblePlusChecked));
|
||||
}
|
||||
|
||||
public async Task FilterBtnAsync()
|
||||
{
|
||||
var lastGood = ProductsDisplay.FilterString;
|
||||
try
|
||||
{
|
||||
await ProductsDisplay.Filter(FilterString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await ProductsDisplay.Filter(lastGood);
|
||||
FilterString = lastGood;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RestoreCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await GetCheckedBooks().RestoreBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
await ReloadAsync();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
public async Task PermanentlyDeleteCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await GetCheckedBooks().PermanentlyDeleteBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
await ReloadAsync();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
private static List<LibraryBook> GetDeletedLibraryBooks()
|
||||
{
|
||||
#if DEBUG
|
||||
if (Avalonia.Controls.Design.IsDesignMode)
|
||||
{
|
||||
return [
|
||||
MockLibraryBook.CreateBook(title: "Mock Audible Plus Library Book 4", isAudiblePlus: true),
|
||||
MockLibraryBook.CreateBook(title: "Mock Audible Plus Library Book 3", isAudiblePlus: true),
|
||||
MockLibraryBook.CreateBook(title: "Mock Library Book 2"),
|
||||
MockLibraryBook.CreateBook(title: "Mock Library Book 1"),
|
||||
];
|
||||
}
|
||||
#endif
|
||||
return DbContexts.GetDeletedLibraryBooks();
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public Task? BindToGridTask { get; set; }
|
||||
public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();
|
||||
public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
|
||||
public ProductsDisplayViewModel ProductsDisplay { get; } = new() { SearchEngine = MainSearchEngine.Instance };
|
||||
|
||||
public double? DownloadProgress { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
|
||||
@@ -34,18 +34,12 @@ namespace LibationAvalonia.ViewModels
|
||||
public bool RemoveColumnVisible { get => field; private set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public List<LibraryBook> GetVisibleBookEntries()
|
||||
=> FilteredInGridEntries?
|
||||
.OfType<LibraryBookEntry>()
|
||||
.Select(lbe => lbe.LibraryBook)
|
||||
.ToList()
|
||||
?? SOURCE
|
||||
.OfType<LibraryBookEntry>()
|
||||
.Select(lbe => lbe.LibraryBook)
|
||||
.ToList();
|
||||
=> GetVisibleGridEntries().Select(lbe => lbe.LibraryBook).ToList();
|
||||
|
||||
private IEnumerable<LibraryBookEntry> GetAllBookEntries()
|
||||
=> SOURCE
|
||||
.BookEntries();
|
||||
public IEnumerable<LibraryBookEntry> GetVisibleGridEntries()
|
||||
=> (FilteredInGridEntries as IEnumerable<GridEntry> ?? SOURCE).OfType<LibraryBookEntry>();
|
||||
|
||||
private IEnumerable<LibraryBookEntry> GetAllBookEntries() => SOURCE.BookEntries();
|
||||
|
||||
public ProductsDisplayViewModel()
|
||||
{
|
||||
@@ -53,6 +47,8 @@ namespace LibationAvalonia.ViewModels
|
||||
VisibleCountChanged?.Invoke(this, 0);
|
||||
}
|
||||
|
||||
public ISearchEngine? SearchEngine { get; set; }
|
||||
|
||||
private static readonly System.Reflection.MethodInfo? SetFlagsMethod;
|
||||
|
||||
/// <summary>
|
||||
@@ -120,7 +116,8 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
//Create the filtered-in list before adding entries to GridEntries to avoid a refresh or UI action
|
||||
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||
var searchResultSet = SearchEngine?.GetSearchResultSet(FilterString);
|
||||
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(searchResultSet);
|
||||
|
||||
// Adding SOURCE to the DataGridViewCollection _after_ building the SOURCE list
|
||||
//Saves ~500 ms on a library of ~4500 books.
|
||||
@@ -315,7 +312,8 @@ namespace LibationAvalonia.ViewModels
|
||||
if (SOURCE.Count == 0)
|
||||
return;
|
||||
|
||||
FilteredInGridEntries = SOURCE.FilterEntries(searchString);
|
||||
var results = SearchEngine?.GetSearchResultSet(searchString);
|
||||
FilteredInGridEntries = SOURCE.FilterEntries(results);
|
||||
|
||||
await refreshGrid();
|
||||
}
|
||||
@@ -334,7 +332,9 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private async void SearchEngineCommands_SearchEngineUpdated(object? sender, EventArgs? e)
|
||||
{
|
||||
var filterResults = SOURCE.FilterEntries(FilterString);
|
||||
|
||||
var searchResultSet = SearchEngine?.GetSearchResultSet(FilterString);
|
||||
var filterResults = SOURCE.FilterEntries(searchResultSet);
|
||||
|
||||
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||
{
|
||||
@@ -454,7 +454,7 @@ namespace LibationAvalonia.ViewModels
|
||||
#endregion
|
||||
|
||||
#region Column Widths
|
||||
|
||||
public bool DisablePersistColumnWidths { get; set; }
|
||||
public DataGridLength TitleWidth { get => getColumnWidth("Title", 200); set => setColumnWidth("Title", value); }
|
||||
public DataGridLength AuthorsWidth { get => getColumnWidth("Authors", 100); set => setColumnWidth("Authors", value); }
|
||||
public DataGridLength NarratorsWidth { get => getColumnWidth("Narrators", 100); set => setColumnWidth("Narrators", value); }
|
||||
@@ -480,6 +480,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private void setColumnWidth(string columnName, DataGridLength width, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (DisablePersistColumnWidths) return;
|
||||
var dictionary = Configuration.Instance.GridColumnsWidths;
|
||||
|
||||
var newValue = (int)width.DisplayValue;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<DataGrid.Styles>
|
||||
<Style Selector="DataGridColumnHeader">
|
||||
<Setter Property="ContextMenu">
|
||||
<ContextMenu Name="GridHeaderContextMenu" Opening="ContextMenu_ContextMenuOpening" Closed="ContextMenu_MenuClosed">
|
||||
<ContextMenu Name="GridHeaderContextMenu" Opening="GridHeaderContextMenu_Opening" Closed="GridHeaderContextMenu_Closed">
|
||||
<ContextMenu.Styles>
|
||||
<Style Selector="MenuItem">
|
||||
<Setter Property="Padding" Value="10,0,-10,0" />
|
||||
@@ -51,6 +51,11 @@
|
||||
</ContextMenu>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="DataGridCell">
|
||||
<Setter Property="ContextMenu">
|
||||
<ContextMenu Opening="GridCellContextMenu_Opening" Opened="GridCellContextMenu_Opened"/>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="DataGridCell > Panel">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||
</Style>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Styling;
|
||||
using DataLayer;
|
||||
@@ -30,13 +28,31 @@ namespace LibationAvalonia.Views
|
||||
public event EventHandler<LibraryBook[]>? ConvertToMp3Clicked;
|
||||
public event EventHandler<LibraryBook>? TagsButtonClicked;
|
||||
|
||||
|
||||
public static readonly StyledProperty<bool> DisableContextMenuProperty =
|
||||
AvaloniaProperty.Register<GroupBox, bool>(nameof(DisableContextMenu));
|
||||
|
||||
public static readonly StyledProperty<bool> DisableColumnCustomizationProperty =
|
||||
AvaloniaProperty.Register<GroupBox, bool>(nameof(DisableColumnCustomization));
|
||||
|
||||
public bool DisableContextMenu
|
||||
{
|
||||
get { return GetValue(DisableContextMenuProperty); }
|
||||
set { SetValue(DisableContextMenuProperty, value); }
|
||||
}
|
||||
|
||||
public bool DisableColumnCustomization
|
||||
{
|
||||
get { return GetValue(DisableColumnCustomizationProperty); }
|
||||
set { SetValue(DisableColumnCustomizationProperty, value); }
|
||||
}
|
||||
|
||||
private ProductsDisplayViewModel? _viewModel => DataContext as ProductsDisplayViewModel;
|
||||
ImageDisplayDialog? imageDisplayDialog;
|
||||
|
||||
public ProductsDisplay()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataGridContextMenus.CellContextMenuStripNeeded += ProductsGrid_CellContextMenuStripNeeded;
|
||||
|
||||
var cellSelector = Selectors.Is<DataGridCell>(null);
|
||||
rowHeightStyle = new Style(_ => cellSelector);
|
||||
@@ -91,6 +107,18 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(Avalonia.Controls.Primitives.TemplateAppliedEventArgs e)
|
||||
{
|
||||
ApplyDisableColumnCustimaziton();
|
||||
base.OnApplyTemplate(e);
|
||||
}
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
if (change.Property == DisableColumnCustomizationProperty)
|
||||
ApplyDisableColumnCustimaziton();
|
||||
base.OnPropertyChanged(change);
|
||||
}
|
||||
|
||||
private void ProductsDisplay_LoadingRow(object sender, DataGridRowEventArgs e)
|
||||
{
|
||||
if (e.Row.DataContext is LibraryBookEntry entry && entry.Liberate?.IsEpisode is true)
|
||||
@@ -180,10 +208,19 @@ namespace LibationAvalonia.Views
|
||||
#endregion
|
||||
|
||||
#region Cell Context Menu
|
||||
|
||||
public void ProductsGrid_CellContextMenuStripNeeded(object? sender, DataGridCellContextMenuStripNeededEventArgs args)
|
||||
public void GridCellContextMenu_Opening(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
var entries = args.GridEntries;
|
||||
e.Cancel = DisableContextMenu;
|
||||
}
|
||||
|
||||
//Use Opened instead of opening because the parent is not set yet in Opening
|
||||
public void GridCellContextMenu_Opened(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not ContextMenu contextMenu ||
|
||||
DataGridCellContextMenu<GridEntry>.Create(contextMenu) is not { } args)
|
||||
return;
|
||||
|
||||
var entries = args.RowItems;
|
||||
var ctx = new GridContextMenu(entries, '_');
|
||||
|
||||
if (App.MainWindow?.Clipboard is IClipboard clipboard)
|
||||
@@ -206,8 +243,8 @@ namespace LibationAvalonia.Views
|
||||
});
|
||||
|
||||
args.ContextMenuItems.Add(new Separator());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#region Liberate all Episodes (Single series only)
|
||||
|
||||
@@ -454,13 +491,13 @@ namespace LibationAvalonia.Views
|
||||
var itemName = column.SortMemberPath;
|
||||
if (itemName == nameof(GridEntry.Remove))
|
||||
continue;
|
||||
|
||||
|
||||
GridHeaderContextMenu.Items.Add(new MenuItem
|
||||
{
|
||||
Header = new CheckBox { Content = new TextBlock { Text = ((string)column.Header).Replace('\n', ' ') } },
|
||||
Tag = column,
|
||||
});
|
||||
|
||||
|
||||
column.IsVisible = Configuration.Instance.GetColumnVisibility(itemName);
|
||||
}
|
||||
|
||||
@@ -478,10 +515,19 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
public void ContextMenu_ContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
private void ApplyDisableColumnCustimaziton()
|
||||
{
|
||||
if (sender is not ContextMenu contextMenu)
|
||||
_viewModel?.DisablePersistColumnWidths = DisableColumnCustomization;
|
||||
productsGrid.CanUserReorderColumns = !DisableColumnCustomization;
|
||||
}
|
||||
|
||||
public void GridHeaderContextMenu_Opening(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
if (DisableContextMenu || sender is not ContextMenu contextMenu)
|
||||
{
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
foreach (var mi in contextMenu.Items.OfType<MenuItem>())
|
||||
{
|
||||
if (mi.Tag is DataGridColumn column && mi.Header is CheckBox cbox)
|
||||
@@ -491,7 +537,7 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
public void ContextMenu_MenuClosed(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public void GridHeaderContextMenu_Closed(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not ContextMenu contextMenu)
|
||||
return;
|
||||
@@ -518,6 +564,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
private void ProductsGrid_ColumnDisplayIndexChanged(object? sender, DataGridColumnEventArgs e)
|
||||
{
|
||||
if (DisableColumnCustomization) return;
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsDisplayIndices;
|
||||
|
||||
@@ -93,6 +93,12 @@ namespace LibationSearchEngine
|
||||
}
|
||||
}
|
||||
|
||||
public SearchEngine(string directory = null)
|
||||
{
|
||||
SearchEngineDirectory = directory
|
||||
?? new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles.Location).CreateSubdirectoryEx("SearchEngine").FullName;
|
||||
}
|
||||
|
||||
/// <summary>Long running. Use await Task.Run(() => UpdateBook(productId))</summary>
|
||||
public void UpdateBook(LibationContext context, string productId)
|
||||
{
|
||||
@@ -131,7 +137,7 @@ namespace LibationSearchEngine
|
||||
public void UpdateTags(string productId, string tags) => updateAnalyzedField(productId, TAGS, tags);
|
||||
|
||||
// all fields are case-specific
|
||||
private static void updateAnalyzedField(string productId, string fieldName, string newValue)
|
||||
private void updateAnalyzedField(string productId, string fieldName, string newValue)
|
||||
=> updateDocument(
|
||||
productId,
|
||||
d =>
|
||||
@@ -170,7 +176,7 @@ namespace LibationSearchEngine
|
||||
d.AddIndexRule(rating, book);
|
||||
});
|
||||
|
||||
private static void updateDocument(string productId, Action<Document> action)
|
||||
private void updateDocument(string productId, Action<Document> action)
|
||||
{
|
||||
var productTerm = new Term(_ID_, productId);
|
||||
|
||||
@@ -277,10 +283,10 @@ namespace LibationSearchEngine
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
private Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
|
||||
// not customizable. don't move to config
|
||||
private static string SearchEngineDirectory { get; }
|
||||
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles.Location).CreateSubdirectoryEx("SearchEngine").FullName;
|
||||
//Defaults to "LibationFiles/SearchEngine, but can be overridden
|
||||
//in constructor for use in TrashBinDialog search
|
||||
private string SearchEngineDirectory { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,16 +45,13 @@ namespace LibationUiBase.GridView
|
||||
=> searchSet is null != otherSet is null ||
|
||||
(searchSet is not null &&
|
||||
otherSet is not null &&
|
||||
searchSet.Intersect(otherSet).Count() != searchSet.Count);
|
||||
searchSet.Intersect(otherSet).Count() != searchSet.Count);
|
||||
|
||||
[return: NotNullIfNotNull(nameof(searchString))]
|
||||
public static HashSet<GridEntry>? FilterEntries(this IEnumerable<GridEntry> entries, string? searchString)
|
||||
[return: NotNullIfNotNull(nameof(searchResultSet))]
|
||||
public static HashSet<GridEntry>? FilterEntries(this IEnumerable<GridEntry> entries, LibationSearchEngine.SearchResultSet? searchResultSet)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
if (searchResultSet is null)
|
||||
return null;
|
||||
|
||||
var searchResultSet = SearchEngineCommands.Search(searchString);
|
||||
|
||||
var booksFilteredIn = entries.IntersectBy(searchResultSet.Docs.Select(d => d.ProductId), l => l.AudibleProductId);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
|
||||
@@ -28,40 +28,35 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
deletedCbl = new System.Windows.Forms.CheckedListBox();
|
||||
label1 = new System.Windows.Forms.Label();
|
||||
restoreBtn = new System.Windows.Forms.Button();
|
||||
permanentlyDeleteBtn = new System.Windows.Forms.Button();
|
||||
everythingCb = new System.Windows.Forms.CheckBox();
|
||||
deletedCheckedLbl = new System.Windows.Forms.Label();
|
||||
productsGrid1 = new LibationWinForms.GridView.ProductsGrid();
|
||||
label2 = new System.Windows.Forms.Label();
|
||||
textBox1 = new System.Windows.Forms.TextBox();
|
||||
button1 = new System.Windows.Forms.Button();
|
||||
audiblePlusCb = new System.Windows.Forms.CheckBox();
|
||||
plusBookcSheckedLbl = new System.Windows.Forms.Label();
|
||||
SuspendLayout();
|
||||
//
|
||||
// deletedCbl
|
||||
//
|
||||
deletedCbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
deletedCbl.FormattingEnabled = true;
|
||||
deletedCbl.Location = new System.Drawing.Point(12, 27);
|
||||
deletedCbl.Name = "deletedCbl";
|
||||
deletedCbl.Size = new System.Drawing.Size(776, 364);
|
||||
deletedCbl.TabIndex = 3;
|
||||
deletedCbl.ItemCheck += deletedCbl_ItemCheck;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new System.Drawing.Point(12, 9);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new System.Drawing.Size(388, 15);
|
||||
label1.TabIndex = 4;
|
||||
label1.TabIndex = 0;
|
||||
label1.Text = "Check books you want to permanently delete from or restore to Libation";
|
||||
//
|
||||
// restoreBtn
|
||||
//
|
||||
restoreBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
restoreBtn.Location = new System.Drawing.Point(572, 398);
|
||||
restoreBtn.Location = new System.Drawing.Point(572, 450);
|
||||
restoreBtn.Name = "restoreBtn";
|
||||
restoreBtn.Size = new System.Drawing.Size(75, 40);
|
||||
restoreBtn.TabIndex = 5;
|
||||
restoreBtn.TabIndex = 6;
|
||||
restoreBtn.Text = "Restore";
|
||||
restoreBtn.UseVisualStyleBackColor = true;
|
||||
restoreBtn.Click += restoreBtn_Click;
|
||||
@@ -69,10 +64,10 @@
|
||||
// permanentlyDeleteBtn
|
||||
//
|
||||
permanentlyDeleteBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
permanentlyDeleteBtn.Location = new System.Drawing.Point(653, 398);
|
||||
permanentlyDeleteBtn.Location = new System.Drawing.Point(653, 450);
|
||||
permanentlyDeleteBtn.Name = "permanentlyDeleteBtn";
|
||||
permanentlyDeleteBtn.Size = new System.Drawing.Size(135, 40);
|
||||
permanentlyDeleteBtn.TabIndex = 5;
|
||||
permanentlyDeleteBtn.TabIndex = 7;
|
||||
permanentlyDeleteBtn.Text = "Permanently Remove\r\nfrom Libation";
|
||||
permanentlyDeleteBtn.UseVisualStyleBackColor = true;
|
||||
permanentlyDeleteBtn.Click += permanentlyDeleteBtn_Click;
|
||||
@@ -81,10 +76,11 @@
|
||||
//
|
||||
everythingCb.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
everythingCb.AutoSize = true;
|
||||
everythingCb.Location = new System.Drawing.Point(12, 410);
|
||||
everythingCb.Location = new System.Drawing.Point(12, 462);
|
||||
everythingCb.Margin = new System.Windows.Forms.Padding(10, 3, 3, 3);
|
||||
everythingCb.Name = "everythingCb";
|
||||
everythingCb.Size = new System.Drawing.Size(82, 19);
|
||||
everythingCb.TabIndex = 6;
|
||||
everythingCb.TabIndex = 4;
|
||||
everythingCb.Text = "Everything";
|
||||
everythingCb.ThreeState = true;
|
||||
everythingCb.UseVisualStyleBackColor = true;
|
||||
@@ -94,23 +90,93 @@
|
||||
//
|
||||
deletedCheckedLbl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
deletedCheckedLbl.AutoSize = true;
|
||||
deletedCheckedLbl.Location = new System.Drawing.Point(126, 411);
|
||||
deletedCheckedLbl.Location = new System.Drawing.Point(100, 463);
|
||||
deletedCheckedLbl.Name = "deletedCheckedLbl";
|
||||
deletedCheckedLbl.Size = new System.Drawing.Size(104, 15);
|
||||
deletedCheckedLbl.TabIndex = 7;
|
||||
deletedCheckedLbl.TabIndex = 0;
|
||||
deletedCheckedLbl.Text = "Checked: {0} of {1}";
|
||||
//
|
||||
// productsGrid1
|
||||
//
|
||||
productsGrid1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
productsGrid1.AutoScroll = true;
|
||||
productsGrid1.DisableColumnCustomization = true;
|
||||
productsGrid1.DisableContextMenu = true;
|
||||
productsGrid1.Location = new System.Drawing.Point(12, 62);
|
||||
productsGrid1.Name = "productsGrid1";
|
||||
productsGrid1.SearchEngine = null;
|
||||
productsGrid1.Size = new System.Drawing.Size(776, 382);
|
||||
productsGrid1.TabIndex = 3;
|
||||
//
|
||||
// label2
|
||||
//
|
||||
label2.AutoSize = true;
|
||||
label2.Location = new System.Drawing.Point(12, 36);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new System.Drawing.Size(123, 15);
|
||||
label2.TabIndex = 0;
|
||||
label2.Text = "Search Deleted Books:";
|
||||
//
|
||||
// textBox1
|
||||
//
|
||||
textBox1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
textBox1.Location = new System.Drawing.Point(141, 33);
|
||||
textBox1.Name = "textBox1";
|
||||
textBox1.Size = new System.Drawing.Size(574, 23);
|
||||
textBox1.TabIndex = 1;
|
||||
textBox1.KeyDown += textBox1_KeyDown;
|
||||
//
|
||||
// button1
|
||||
//
|
||||
button1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
button1.Location = new System.Drawing.Point(721, 33);
|
||||
button1.Name = "button1";
|
||||
button1.Size = new System.Drawing.Size(67, 23);
|
||||
button1.TabIndex = 2;
|
||||
button1.Text = "Filter";
|
||||
button1.UseVisualStyleBackColor = true;
|
||||
button1.Click += searchBtn_Click;
|
||||
//
|
||||
// audiblePlusCb
|
||||
//
|
||||
audiblePlusCb.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
audiblePlusCb.AutoSize = true;
|
||||
audiblePlusCb.Location = new System.Drawing.Point(247, 462);
|
||||
audiblePlusCb.Margin = new System.Windows.Forms.Padding(10, 3, 3, 3);
|
||||
audiblePlusCb.Name = "audiblePlusCb";
|
||||
audiblePlusCb.Size = new System.Drawing.Size(127, 19);
|
||||
audiblePlusCb.TabIndex = 5;
|
||||
audiblePlusCb.Text = "Audible Plus Books";
|
||||
audiblePlusCb.ThreeState = true;
|
||||
audiblePlusCb.UseVisualStyleBackColor = true;
|
||||
audiblePlusCb.CheckStateChanged += audiblePlusCb_CheckStateChanged;
|
||||
//
|
||||
// plusBookcSheckedLbl
|
||||
//
|
||||
plusBookcSheckedLbl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
plusBookcSheckedLbl.AutoSize = true;
|
||||
plusBookcSheckedLbl.Location = new System.Drawing.Point(380, 463);
|
||||
plusBookcSheckedLbl.Name = "plusBookcSheckedLbl";
|
||||
plusBookcSheckedLbl.Size = new System.Drawing.Size(104, 15);
|
||||
plusBookcSheckedLbl.TabIndex = 0;
|
||||
plusBookcSheckedLbl.Text = "Checked: {0} of {1}";
|
||||
//
|
||||
// TrashBinDialog
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
ClientSize = new System.Drawing.Size(800, 450);
|
||||
ClientSize = new System.Drawing.Size(800, 502);
|
||||
Controls.Add(plusBookcSheckedLbl);
|
||||
Controls.Add(button1);
|
||||
Controls.Add(textBox1);
|
||||
Controls.Add(label2);
|
||||
Controls.Add(productsGrid1);
|
||||
Controls.Add(deletedCheckedLbl);
|
||||
Controls.Add(audiblePlusCb);
|
||||
Controls.Add(everythingCb);
|
||||
Controls.Add(permanentlyDeleteBtn);
|
||||
Controls.Add(restoreBtn);
|
||||
Controls.Add(label1);
|
||||
Controls.Add(deletedCbl);
|
||||
Name = "TrashBinDialog";
|
||||
Text = "Trash Bin";
|
||||
ResumeLayout(false);
|
||||
@@ -118,12 +184,16 @@
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.CheckedListBox deletedCbl;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Button restoreBtn;
|
||||
private System.Windows.Forms.Button permanentlyDeleteBtn;
|
||||
private System.Windows.Forms.CheckBox everythingCb;
|
||||
private System.Windows.Forms.Label deletedCheckedLbl;
|
||||
private GridView.ProductsGrid productsGrid1;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.TextBox textBox1;
|
||||
private System.Windows.Forms.Button button1;
|
||||
private System.Windows.Forms.CheckBox audiblePlusCb;
|
||||
private System.Windows.Forms.Label plusBookcSheckedLbl;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using System.Collections;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class TrashBinDialog : Form
|
||||
{
|
||||
private readonly string deletedCheckedTemplate;
|
||||
private string lastGoodFilter = "";
|
||||
private TempSearchEngine SearchEngine { get; } = new TempSearchEngine();
|
||||
public TrashBinDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -21,29 +24,67 @@ namespace LibationWinForms.Dialogs
|
||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
|
||||
deletedCheckedTemplate = deletedCheckedLbl.Text;
|
||||
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
foreach (var lb in deletedBooks)
|
||||
deletedCbl.Items.Add(lb);
|
||||
|
||||
setLabel();
|
||||
deletedCheckedLbl.Text = "";
|
||||
plusBookcSheckedLbl.Text = "";
|
||||
productsGrid1.SearchEngine = SearchEngine;
|
||||
productsGrid1.RemovableCountChanged += (_, _) => UpdateCounts();
|
||||
productsGrid1.VisibleCountChanged += (_, _) => UpdateCounts();
|
||||
Load += TrashBinDialog_Load;
|
||||
}
|
||||
|
||||
private void deletedCbl_ItemCheck(object sender, ItemCheckEventArgs e)
|
||||
private IEnumerable<LibraryBook> GetCheckedBooks() => productsGrid1.GetVisibleGridEntries().Where(i => i.Remove is true).Select(i => i.LibraryBook);
|
||||
|
||||
private async void TrashBinDialog_Load(object? sender, EventArgs e)
|
||||
{
|
||||
// CheckedItems.Count is not updated until after the event fires
|
||||
setLabel(e.NewValue);
|
||||
productsGrid1.RemoveColumnVisible = true;
|
||||
await InitAsync();
|
||||
}
|
||||
|
||||
private void UpdateCounts()
|
||||
{
|
||||
var visible = productsGrid1.GetVisibleGridEntries().ToArray();
|
||||
var plusVisibleCount = visible.Count(e => e.LibraryBook.IsAudiblePlus);
|
||||
|
||||
var checkedCount = visible.Count(e => e.Remove is true);
|
||||
var plusCheckedCount = visible.Count(e => e.LibraryBook.IsAudiblePlus && e.Remove is true);
|
||||
|
||||
deletedCheckedLbl.Text = $"Checked: {checkedCount} of {visible.Length}";
|
||||
plusBookcSheckedLbl.Text = $"Checked: {plusCheckedCount} of {plusVisibleCount}";
|
||||
|
||||
everythingCb.CheckStateChanged -= everythingCb_CheckStateChanged;
|
||||
everythingCb.CheckState = checkedCount == 0 || visible.Length == 0 ? CheckState.Unchecked
|
||||
: checkedCount == visible.Length ? CheckState.Checked
|
||||
: CheckState.Indeterminate;
|
||||
everythingCb.CheckStateChanged += everythingCb_CheckStateChanged;
|
||||
|
||||
audiblePlusCb.CheckStateChanged -= audiblePlusCb_CheckStateChanged;
|
||||
audiblePlusCb.CheckState = plusCheckedCount == 0 || plusVisibleCount == 0 ? CheckState.Unchecked
|
||||
: plusCheckedCount == plusVisibleCount ? CheckState.Checked
|
||||
: CheckState.Indeterminate;
|
||||
audiblePlusCb.CheckStateChanged += audiblePlusCb_CheckStateChanged;
|
||||
}
|
||||
|
||||
private async Task InitAsync()
|
||||
{
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
SearchEngine.ReindexSearchEngine(deletedBooks);
|
||||
await productsGrid1.BindToGridAsync(deletedBooks);
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
SearchEngine.ReindexSearchEngine(deletedBooks);
|
||||
productsGrid1.UpdateGrid(deletedBooks);
|
||||
}
|
||||
|
||||
private async void permanentlyDeleteBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
setControlsEnabled(false);
|
||||
|
||||
var removed = deletedCbl.CheckedItems.Cast<LibraryBook>().ToList();
|
||||
|
||||
removeFromCheckList(removed);
|
||||
await removed.PermanentlyDeleteBooksAsync();
|
||||
var qtyChanges = await GetCheckedBooks().PermanentlyDeleteBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
|
||||
setControlsEnabled(true);
|
||||
}
|
||||
@@ -52,65 +93,70 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
setControlsEnabled(false);
|
||||
|
||||
var removed = deletedCbl.CheckedItems.Cast<LibraryBook>().ToList();
|
||||
|
||||
removeFromCheckList(removed);
|
||||
await removed.RestoreBooksAsync();
|
||||
var qtyChanges = await GetCheckedBooks().RestoreBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
|
||||
setControlsEnabled(true);
|
||||
}
|
||||
|
||||
private void removeFromCheckList(IEnumerable objects)
|
||||
{
|
||||
foreach (var o in objects)
|
||||
deletedCbl.Items.Remove(o);
|
||||
|
||||
deletedCbl.Refresh();
|
||||
setLabel();
|
||||
}
|
||||
|
||||
private void setControlsEnabled(bool enabled)
|
||||
=> restoreBtn.Enabled = permanentlyDeleteBtn.Enabled = deletedCbl.Enabled = everythingCb.Enabled = enabled;
|
||||
=> Invoke(() => productsGrid1.Enabled = restoreBtn.Enabled = permanentlyDeleteBtn.Enabled = everythingCb.Enabled = enabled);
|
||||
|
||||
private void everythingCb_CheckStateChanged(object sender, EventArgs e)
|
||||
private void textBox1_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (everythingCb.CheckState is CheckState.Indeterminate)
|
||||
{
|
||||
everythingCb.CheckState = CheckState.Unchecked;
|
||||
return;
|
||||
}
|
||||
|
||||
deletedCbl.ItemCheck -= deletedCbl_ItemCheck;
|
||||
|
||||
for (var i = 0; i < deletedCbl.Items.Count; i++)
|
||||
deletedCbl.SetItemChecked(i, everythingCb.CheckState is CheckState.Checked);
|
||||
|
||||
setLabel();
|
||||
|
||||
deletedCbl.ItemCheck += deletedCbl_ItemCheck;
|
||||
if (e.KeyCode == Keys.Enter)
|
||||
searchBtn_Click(sender, e);
|
||||
}
|
||||
|
||||
|
||||
private void setLabel(CheckState? checkedState = null)
|
||||
private void searchBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
var pre = deletedCbl.CheckedItems.Count;
|
||||
int count = checkedState switch
|
||||
try
|
||||
{
|
||||
CheckState.Checked => pre + 1,
|
||||
CheckState.Unchecked => pre - 1,
|
||||
_ => pre,
|
||||
};
|
||||
productsGrid1.Filter(textBox1.Text);
|
||||
lastGoodFilter = textBox1.Text;
|
||||
}
|
||||
catch
|
||||
{
|
||||
productsGrid1.Filter(lastGoodFilter);
|
||||
}
|
||||
}
|
||||
|
||||
everythingCb.CheckStateChanged -= everythingCb_CheckStateChanged;
|
||||
private void audiblePlusCb_CheckStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
switch (audiblePlusCb.CheckState)
|
||||
{
|
||||
case CheckState.Checked:
|
||||
SetVisibleChecked(e => e.IsAudiblePlus, isChecked: true);
|
||||
break;
|
||||
case CheckState.Unchecked:
|
||||
SetVisibleChecked(e => e.IsAudiblePlus, isChecked: false);
|
||||
break;
|
||||
default:
|
||||
audiblePlusCb.CheckState = CheckState.Unchecked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
private void everythingCb_CheckStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
switch (everythingCb.CheckState)
|
||||
{
|
||||
case CheckState.Checked:
|
||||
SetVisibleChecked(_ => true, isChecked: true);
|
||||
break;
|
||||
case CheckState.Unchecked:
|
||||
SetVisibleChecked(_ => true, isChecked: false);
|
||||
break;
|
||||
default:
|
||||
everythingCb.CheckState = CheckState.Unchecked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
everythingCb.CheckState
|
||||
= count > 0 && count == deletedCbl.Items.Count ? CheckState.Checked
|
||||
: count == 0 ? CheckState.Unchecked
|
||||
: CheckState.Indeterminate;
|
||||
|
||||
everythingCb.CheckStateChanged += everythingCb_CheckStateChanged;
|
||||
|
||||
deletedCheckedLbl.Text = string.Format(deletedCheckedTemplate, count, deletedCbl.Items.Count);
|
||||
public void SetVisibleChecked(Func<LibraryBook, bool> predicate, bool isChecked)
|
||||
{
|
||||
productsGrid1.GetVisibleGridEntries().Where(e => predicate(e.LibraryBook)).ForEach(i => i.Remove = isChecked);
|
||||
UpdateCounts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,64 @@
|
||||
<root>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<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">
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
/*
|
||||
@@ -43,8 +44,10 @@ namespace LibationWinForms.GridView
|
||||
.OfType<LibraryBookEntry>()
|
||||
.Union(Items.OfType<LibraryBookEntry>());
|
||||
|
||||
public ISearchEngine? SearchEngine { get; set; }
|
||||
|
||||
public bool SupportsFiltering => true;
|
||||
public string Filter
|
||||
public string? Filter
|
||||
{
|
||||
get => FilterString;
|
||||
set
|
||||
@@ -54,7 +57,8 @@ namespace LibationWinForms.GridView
|
||||
if (Items.Count + FilterRemoved.Count == 0)
|
||||
return;
|
||||
|
||||
FilteredInGridEntries = AllItems().FilterEntries(FilterString);
|
||||
var searchResultSet = SearchEngine?.GetSearchResultSet(FilterString);
|
||||
FilteredInGridEntries = AllItems().FilterEntries(searchResultSet);
|
||||
refreshEntries();
|
||||
}
|
||||
}
|
||||
@@ -63,16 +67,16 @@ namespace LibationWinForms.GridView
|
||||
protected override bool SupportsSortingCore => true;
|
||||
protected override bool SupportsSearchingCore => true;
|
||||
protected override bool IsSortedCore => isSorted;
|
||||
protected override PropertyDescriptor SortPropertyCore => propertyDescriptor;
|
||||
protected override PropertyDescriptor? SortPropertyCore => propertyDescriptor;
|
||||
protected override ListSortDirection SortDirectionCore => Comparer.SortOrder;
|
||||
|
||||
/// <summary> Items that were removed from the base list due to filtering </summary>
|
||||
private readonly List<GridEntry> FilterRemoved = new();
|
||||
private string FilterString;
|
||||
private string? FilterString;
|
||||
private bool isSorted;
|
||||
private PropertyDescriptor propertyDescriptor;
|
||||
private PropertyDescriptor? propertyDescriptor;
|
||||
/// <summary> All GridEntries present in the current filter set. If null, no filter is applied and all entries are filtered in.(This was a performance choice)</summary>
|
||||
private HashSet<GridEntry> FilteredInGridEntries;
|
||||
private HashSet<GridEntry>? FilteredInGridEntries;
|
||||
|
||||
#region Unused - Advanced Filtering
|
||||
public bool SupportsAdvancedSorting => false;
|
||||
@@ -128,7 +132,7 @@ namespace LibationWinForms.GridView
|
||||
//(except for episodes that are collapsed)
|
||||
foreach (var addBack in addBackEntries)
|
||||
{
|
||||
if (addBack is LibraryBookEntry lbe && lbe.Parent is SeriesEntry se && !se.Liberate.Expanded)
|
||||
if (addBack is LibraryBookEntry lbe && lbe.Parent is SeriesEntry se && se.Liberate?.Expanded is not true)
|
||||
continue;
|
||||
|
||||
FilterRemoved.Remove(addBack);
|
||||
@@ -137,9 +141,10 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||
private void SearchEngineCommands_SearchEngineUpdated(object? sender, EventArgs e)
|
||||
{
|
||||
var filterResults = AllItems().FilterEntries(FilterString);
|
||||
var searchResultSet = SearchEngine?.GetSearchResultSet(FilterString);
|
||||
var filterResults = AllItems().FilterEntries(searchResultSet);
|
||||
|
||||
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||
{
|
||||
@@ -168,7 +173,7 @@ namespace LibationWinForms.GridView
|
||||
base.Remove(episode);
|
||||
}
|
||||
|
||||
sEntry.Liberate.Expanded = false;
|
||||
sEntry.Liberate?.Expanded = false;
|
||||
}
|
||||
|
||||
public void ExpandItem(SeriesEntry sEntry)
|
||||
@@ -183,7 +188,7 @@ namespace LibationWinForms.GridView
|
||||
InsertItem(++sindex, episode);
|
||||
}
|
||||
}
|
||||
sEntry.Liberate.Expanded = true;
|
||||
sEntry.Liberate?.Expanded = true;
|
||||
}
|
||||
|
||||
public void RemoveFilter()
|
||||
@@ -216,7 +221,7 @@ namespace LibationWinForms.GridView
|
||||
itemsList.AddRange(sortedItems);
|
||||
}
|
||||
|
||||
private void GridEntryBindingList_ListChanged(object sender, ListChangedEventArgs e)
|
||||
private void GridEntryBindingList_ListChanged(object? sender, ListChangedEventArgs e)
|
||||
{
|
||||
if (e.ListChangedType == ListChangedType.ItemChanged && IsSortedCore && e.PropertyDescriptor == SortPropertyCore)
|
||||
refreshEntries();
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace LibationWinForms.GridView
|
||||
public ProductsDisplay()
|
||||
{
|
||||
InitializeComponent();
|
||||
productsGrid.SearchEngine = MainSearchEngine.Instance;
|
||||
}
|
||||
|
||||
#region Button controls
|
||||
@@ -432,7 +433,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
#endregion
|
||||
|
||||
internal List<LibraryBook> GetVisible() => productsGrid.GetVisibleBooks().ToList();
|
||||
internal List<LibraryBook> GetVisible() => productsGrid.GetVisibleBookEntries().ToList();
|
||||
|
||||
private void productsGrid_VisibleCountChanged(object sender, int count)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using DataLayer;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.Core.WindowsDesktop.Forms;
|
||||
@@ -6,6 +7,7 @@ using LibationFileManager;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
@@ -22,6 +24,23 @@ namespace LibationWinForms.GridView
|
||||
|
||||
public partial class ProductsGrid : UserControl
|
||||
{
|
||||
[DefaultValue(false)]
|
||||
[Category("Behavior")]
|
||||
[Description("Disable the grid context menu")]
|
||||
public bool DisableContextMenu { get; set; }
|
||||
[DefaultValue(false)]
|
||||
[Category("Behavior")]
|
||||
[Description("Disable grid column reordering and don't persist width changes")]
|
||||
public bool DisableColumnCustomization
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
gridEntryDataGridView.AllowUserToOrderColumns = !value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int>? VisibleCountChanged;
|
||||
public event LibraryBookEntryClickedEventHandler? LiberateClicked;
|
||||
@@ -33,13 +52,17 @@ namespace LibationWinForms.GridView
|
||||
public event ProductsGridCellContextMenuStripNeededEventHandler? LiberateContextMenuStripNeeded;
|
||||
|
||||
private GridEntryBindingList? bindingList;
|
||||
internal IEnumerable<LibraryBook> GetVisibleBooks()
|
||||
=> bindingList
|
||||
?.GetFilteredInItems()
|
||||
.Select(lbe => lbe.LibraryBook) ?? Enumerable.Empty<LibraryBook>();
|
||||
internal IEnumerable<LibraryBook> GetVisibleBookEntries()
|
||||
=> GetVisibleGridEntries().Select(lbe => lbe.LibraryBook);
|
||||
|
||||
public IEnumerable<LibraryBookEntry> GetVisibleGridEntries()
|
||||
=> bindingList?.GetFilteredInItems().OfType<LibraryBookEntry>() ?? [];
|
||||
|
||||
internal IEnumerable<LibraryBookEntry> GetAllBookEntries()
|
||||
=> bindingList?.AllItems().BookEntries() ?? Enumerable.Empty<LibraryBookEntry>();
|
||||
|
||||
public ISearchEngine? SearchEngine { get => field; set { field = value; bindingList?.SearchEngine = value; } }
|
||||
|
||||
public ProductsGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -47,19 +70,7 @@ namespace LibationWinForms.GridView
|
||||
gridEntryDataGridView.Scroll += (_, s) => Scroll?.Invoke(this, s);
|
||||
gridEntryDataGridView.CellContextMenuStripNeeded += GridEntryDataGridView_CellContextMenuStripNeeded;
|
||||
removeGVColumn.Frozen = false;
|
||||
|
||||
defaultFont = gridEntryDataGridView.DefaultCellStyle.Font ?? gridEntryDataGridView.Font;
|
||||
setGridFontScale(Configuration.Instance.GridFontScaleFactor);
|
||||
setGridScale(Configuration.Instance.GridScaleFactor);
|
||||
Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged += Configuration_FontScaleChanged;
|
||||
gridEntryDataGridView.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
|
||||
gridEntryDataGridView.Disposed += (_, _) =>
|
||||
{
|
||||
Configuration.Instance.PropertyChanged -= Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged -= Configuration_FontScaleChanged;
|
||||
};
|
||||
defaultFont = gridEntryDataGridView.DefaultCellStyle.Font ?? gridEntryDataGridView.Font;
|
||||
}
|
||||
|
||||
#region Scaling
|
||||
@@ -120,7 +131,7 @@ namespace LibationWinForms.GridView
|
||||
private void GridEntryDataGridView_CellContextMenuStripNeeded(object? sender, DataGridViewCellContextMenuStripNeededEventArgs e)
|
||||
{
|
||||
// header
|
||||
if (e.RowIndex < 0 || sender is not DataGridView dgv)
|
||||
if (DisableContextMenu || e.RowIndex < 0 || sender is not DataGridView dgv)
|
||||
return;
|
||||
|
||||
e.ContextMenuStrip = new ContextMenuStrip();
|
||||
@@ -313,7 +324,7 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
System.Threading.SynchronizationContext.SetSynchronizationContext(null);
|
||||
|
||||
bindingList = new GridEntryBindingList(geList);
|
||||
bindingList = new GridEntryBindingList(geList) { SearchEngine = SearchEngine };
|
||||
bindingList.CollapseAll();
|
||||
|
||||
//The syncBindingSource ensures that the IGridEntry list is added on the UI thread
|
||||
@@ -381,7 +392,8 @@ namespace LibationWinForms.GridView
|
||||
|
||||
RemoveBooks(removedBooks);
|
||||
|
||||
gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
|
||||
if (topRow >= 0 && topRow < gridEntryDataGridView.RowCount)
|
||||
gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
|
||||
}
|
||||
|
||||
public void RemoveBooks(IEnumerable<LibraryBookEntry> removedBooks)
|
||||
@@ -505,8 +517,21 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private void ProductsGrid_Load(object sender, EventArgs e)
|
||||
{
|
||||
//https://stackoverflow.com/a/4498512/3335599
|
||||
if (System.ComponentModel.LicenseManager.UsageMode == System.ComponentModel.LicenseUsageMode.Designtime) return;
|
||||
//DesignMode is not set in constructor
|
||||
if (DesignMode)
|
||||
return;
|
||||
|
||||
setGridFontScale(Configuration.Instance.GridFontScaleFactor);
|
||||
setGridScale(Configuration.Instance.GridScaleFactor);
|
||||
Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged += Configuration_FontScaleChanged;
|
||||
gridEntryDataGridView.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
|
||||
gridEntryDataGridView.Disposed += (_, _) =>
|
||||
{
|
||||
Configuration.Instance.PropertyChanged -= Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged -= Configuration_FontScaleChanged;
|
||||
};
|
||||
|
||||
gridEntryDataGridView.ColumnWidthChanged += gridEntryDataGridView_ColumnWidthChanged;
|
||||
gridEntryDataGridView.ColumnDisplayIndexChanged += gridEntryDataGridView_ColumnDisplayIndexChanged;
|
||||
@@ -523,6 +548,8 @@ namespace LibationWinForms.GridView
|
||||
|
||||
foreach (DataGridViewColumn column in gridEntryDataGridView.Columns)
|
||||
{
|
||||
if (column == removeGVColumn)
|
||||
continue;
|
||||
var itemName = column.DataPropertyName;
|
||||
var visible = config.GetColumnVisibility(itemName);
|
||||
|
||||
@@ -596,6 +623,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object? sender, DataGridViewColumnEventArgs e)
|
||||
{
|
||||
if (DisableColumnCustomization) return;
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsDisplayIndices;
|
||||
@@ -613,6 +641,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private void gridEntryDataGridView_ColumnWidthChanged(object? sender, DataGridViewColumnEventArgs e)
|
||||
{
|
||||
if (DisableColumnCustomization) return;
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsWidths;
|
||||
|
||||
Reference in New Issue
Block a user