Merge pull request #249 from Mbucari/master

Add FilterableSortableBindingList to handle filtering the DataGridView
This commit is contained in:
rmcrackan 2022-05-16 22:08:31 -04:00 committed by GitHub
commit a89b07394f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 265 additions and 318 deletions

View File

@ -1,164 +0,0 @@
using System;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core.Net.Http;
using Dinah.Core.Threading;
using FileLiberator;
namespace LibationWinForms.BookLiberation.BaseForms
{
public class LiberationBaseForm : Form
{
protected Streamable Streamable { get; private set; }
protected LogMe LogMe { get; private set; }
private SynchronizeInvoker Invoker { get; init; }
public LiberationBaseForm()
{
//SynchronizationContext.Current will be null until the process contains a Form.
//If this is the first form created, it will not exist until after execution
//reaches inside the constructor (after base class has been initialized).
Invoker = new SynchronizeInvoker();
this.SetLibationIcon();
}
public void RegisterFileLiberator(Streamable streamable, LogMe logMe = null)
{
if (streamable is null) return;
Streamable = streamable;
LogMe = logMe;
Subscribe(streamable);
if (Streamable is Processable processable)
Subscribe(processable);
if (Streamable is AudioDecodable audioDecodable)
Subscribe(audioDecodable);
}
#region Event Subscribers and Unsubscribers
private void Subscribe(Streamable streamable)
{
UnsubscribeStreamable(this, EventArgs.Empty);
streamable.StreamingBegin += OnStreamingBeginShow;
streamable.StreamingBegin += Streamable_StreamingBegin;
streamable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
streamable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
streamable.StreamingCompleted += Streamable_StreamingCompleted;
streamable.StreamingCompleted += OnStreamingCompletedClose;
Disposed += UnsubscribeStreamable;
}
private void Subscribe(Processable processable)
{
UnsubscribeProcessable(this, null);
processable.Begin += Processable_Begin;
processable.StatusUpdate += Processable_StatusUpdate;
processable.Completed += Processable_Completed;
//The form is created on Processable.Begin and we
//dispose of it on Processable.Completed
processable.Completed += OnCompletedDispose;
//Don't unsubscribe from Dispose because it fires when
//Streamable.StreamingCompleted closes the form, and
//the Processable events need to live past that event.
processable.Completed += UnsubscribeProcessable;
}
private void Subscribe(AudioDecodable audioDecodable)
{
UnsubscribeAudioDecodable(this, EventArgs.Empty);
audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
Disposed += UnsubscribeAudioDecodable;
}
private void UnsubscribeStreamable(object sender, EventArgs e)
{
Disposed -= UnsubscribeStreamable;
Streamable.StreamingBegin -= OnStreamingBeginShow;
Streamable.StreamingBegin -= Streamable_StreamingBegin;
Streamable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
Streamable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
Streamable.StreamingCompleted -= Streamable_StreamingCompleted;
Streamable.StreamingCompleted -= OnStreamingCompletedClose;
}
private void UnsubscribeProcessable(object sender, LibraryBook e)
{
if (Streamable is not Processable processable)
return;
processable.Completed -= UnsubscribeProcessable;
processable.Completed -= OnCompletedDispose;
processable.Completed -= Processable_Completed;
processable.StatusUpdate -= Processable_StatusUpdate;
processable.Begin -= Processable_Begin;
}
private void UnsubscribeAudioDecodable(object sender, EventArgs e)
{
if (Streamable is not AudioDecodable audioDecodable)
return;
Disposed -= UnsubscribeAudioDecodable;
audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
audioDecodable.Cancel();
}
#endregion
#region Form creation and disposal handling
/// <summary>
/// If the form was shown using Show (not ShowDialog), Form.Close calls Form.Dispose
/// </summary>
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThreadAsync(Close);
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThreadAsync(Dispose);
/// <summary>
/// If StreamingBegin is fired from a worker thread, the window will be created on that
/// worker thread. We need to make certain that we show the window on the UI thread (same
/// thread that created form), otherwise the renderer will be on a worker thread which
/// could cause it to freeze. Form.BeginInvoke won't work until the form is created
/// (ie. shown) because Control doesn't get a window handle until it is Shown.
/// </summary>
private void OnStreamingBeginShow(object sender, string beginString) => Invoker.UIThreadAsync(Show);
#endregion
#region Streamable event handlers
public virtual void Streamable_StreamingBegin(object sender, string beginString) { }
public virtual void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress) { }
public virtual void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining) { }
public virtual void Streamable_StreamingCompleted(object sender, string completedString) { }
#endregion
#region Processable event handlers
public virtual void Processable_Begin(object sender, LibraryBook libraryBook) { }
public virtual void Processable_StatusUpdate(object sender, string statusUpdate) { }
public virtual void Processable_Completed(object sender, LibraryBook libraryBook) { }
#endregion
#region AudioDecodable event handlers
public virtual void AudioDecodable_TitleDiscovered(object sender, string title) { }
public virtual void AudioDecodable_AuthorsDiscovered(object sender, string authors) { }
public virtual void AudioDecodable_NarratorsDiscovered(object sender, string narrators) { }
public virtual void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt) { }
public virtual void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate) { }
#endregion
}
}

View File

@ -2,30 +2,48 @@
using System.Windows.Forms; using System.Windows.Forms;
using Dinah.Core.Net.Http; using Dinah.Core.Net.Http;
using Dinah.Core.Threading; using Dinah.Core.Threading;
using LibationWinForms.BookLiberation.BaseForms; using FileLiberator;
namespace LibationWinForms.BookLiberation namespace LibationWinForms.BookLiberation
{ {
public partial class DownloadForm : LiberationBaseForm public partial class DownloadForm : Form
{ {
protected Streamable Streamable { get; private set; }
protected LogMe LogMe { get; private set; }
private SynchronizeInvoker Invoker { get; init; }
public DownloadForm() public DownloadForm()
{ {
//SynchronizationContext.Current will be null until the process contains a Form.
//If this is the first form created, it will not exist until after execution
//reaches inside the constructor (after base class has been initialized).
Invoker = new SynchronizeInvoker();
InitializeComponent(); InitializeComponent();
this.SetLibationIcon();
progressLbl.Text = ""; progressLbl.Text = "";
filenameLbl.Text = ""; filenameLbl.Text = "";
} }
public void RegisterFileLiberator(Streamable streamable, LogMe logMe = null)
{
if (streamable is null) return;
streamable.StreamingBegin += Streamable_StreamingBegin;
streamable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
streamable.StreamingCompleted += (_, _) => this.UIThreadAsync(Close);
Streamable = streamable;
LogMe = logMe;
}
#region Streamable event handler overrides #region Streamable event handler overrides
public override void Streamable_StreamingBegin(object sender, string beginString) public void Streamable_StreamingBegin(object sender, string beginString)
{ {
base.Streamable_StreamingBegin(sender, beginString); Invoker.UIThreadAsync(Show);
filenameLbl.UIThreadAsync(() => filenameLbl.Text = beginString); filenameLbl.UIThreadAsync(() => filenameLbl.Text = beginString);
} }
public override void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress) public void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress)
{ {
base.Streamable_StreamingProgressChanged(sender, downloadProgress);
// this won't happen with download file. it will happen with download string // this won't happen with download file. it will happen with download string
if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0) if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
return; return;

View File

@ -73,7 +73,7 @@
this.addQuickFilterBtn = new System.Windows.Forms.Button(); this.addQuickFilterBtn = new System.Windows.Forms.Button();
this.splitContainer1 = new System.Windows.Forms.SplitContainer(); this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.panel1 = new System.Windows.Forms.Panel(); this.panel1 = new System.Windows.Forms.Panel();
this.hideQueueBtn = new System.Windows.Forms.Button(); this.toggleQueueHideBtn = new System.Windows.Forms.Button();
this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl(); this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl();
this.menuStrip1.SuspendLayout(); this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout(); this.statusStrip1.SuspendLayout();
@ -462,7 +462,7 @@
// panel1 // panel1
// //
this.panel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; this.panel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.panel1.Controls.Add(this.hideQueueBtn); this.panel1.Controls.Add(this.toggleQueueHideBtn);
this.panel1.Controls.Add(this.gridPanel); this.panel1.Controls.Add(this.gridPanel);
this.panel1.Controls.Add(this.addQuickFilterBtn); this.panel1.Controls.Add(this.addQuickFilterBtn);
this.panel1.Controls.Add(this.filterHelpBtn); this.panel1.Controls.Add(this.filterHelpBtn);
@ -477,15 +477,15 @@
// //
// hideQueueBtn // hideQueueBtn
// //
this.hideQueueBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.hideQueueBtn.Location = new System.Drawing.Point(966, 4); this.toggleQueueHideBtn.Location = new System.Drawing.Point(966, 4);
this.hideQueueBtn.Margin = new System.Windows.Forms.Padding(5, 4, 17, 4); this.toggleQueueHideBtn.Margin = new System.Windows.Forms.Padding(5, 4, 17, 4);
this.hideQueueBtn.Name = "hideQueueBtn"; this.toggleQueueHideBtn.Name = "hideQueueBtn";
this.hideQueueBtn.Size = new System.Drawing.Size(38, 36); this.toggleQueueHideBtn.Size = new System.Drawing.Size(38, 36);
this.hideQueueBtn.TabIndex = 8; this.toggleQueueHideBtn.TabIndex = 8;
this.hideQueueBtn.Text = "❰❰❰"; this.toggleQueueHideBtn.Text = "❱❱❱";
this.hideQueueBtn.UseVisualStyleBackColor = true; this.toggleQueueHideBtn.UseVisualStyleBackColor = true;
this.hideQueueBtn.Click += new System.EventHandler(this.HideQueueBtn_Click); this.toggleQueueHideBtn.Click += new System.EventHandler(this.ToggleQueueHideBtn_Click);
// //
// processBookQueue1 // processBookQueue1
// //
@ -571,6 +571,6 @@
private System.Windows.Forms.SplitContainer splitContainer1; private System.Windows.Forms.SplitContainer splitContainer1;
private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1; private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1;
private System.Windows.Forms.Panel panel1; private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Button hideQueueBtn; private System.Windows.Forms.Button toggleQueueHideBtn;
} }
} }

View File

@ -11,12 +11,18 @@ namespace LibationWinForms
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread //GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e) private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await Task.Run(() => processBookQueue1.AddDownloadDecrypt(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking() {
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddDownloadDecrypt(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
.Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.NotLiberated))); .Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.NotLiberated)));
}
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e) private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await Task.Run(() => processBookQueue1.AddDownloadPdf(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking() {
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddDownloadPdf(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
.Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated))); .Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated)));
}
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e) private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
{ {
@ -29,8 +35,11 @@ namespace LibationWinForms
MessageBoxButtons.YesNo, MessageBoxButtons.YesNo,
MessageBoxIcon.Warning); MessageBoxIcon.Warning);
if (result == DialogResult.Yes) if (result == DialogResult.Yes)
{
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddConvertMp3(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking() await Task.Run(() => processBookQueue1.AddConvertMp3(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
.Where(lb => lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.Liberated))); .Where(lb => lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.Liberated)));
}
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing. //Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
} }
} }

View File

@ -1,41 +1,73 @@
using ApplicationServices; using DataLayer;
using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using LibationWinForms.ProcessQueue; using LibationWinForms.ProcessQueue;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms namespace LibationWinForms
{ {
public partial class Form1 public partial class Form1
{ {
private void Configure_ProcessQueue()
{
productsGrid.LiberateClicked += (_, lb) => processBookQueue1.AddDownloadDecrypt(lb);
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
}
int WidthChange = 0; int WidthChange = 0;
private void HideQueueBtn_Click(object sender, EventArgs e) private void Configure_ProcessQueue()
{ {
if (splitContainer1.Panel2Collapsed) productsGrid.LiberateClicked += ProductsGrid_LiberateClicked;
{ processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
WidthChange = WidthChange == 0 ? splitContainer1.Panel2.Width + splitContainer1.SplitterWidth : WidthChange; var coppalseState = Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.Panel2Collapsed));
Width += WidthChange; WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
splitContainer1.Panel2.Controls.Add(processBookQueue1); SetQueueCollapseState(coppalseState);
splitContainer1.Panel2Collapsed = false;
processBookQueue1.popoutBtn.Visible = true;
hideQueueBtn.Text = "❰❰❰";
} }
else
private void ProductsGrid_LiberateClicked(object sender, LibraryBook e)
{
if (e.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
{
SetQueueCollapseState(false);
processBookQueue1.AddDownloadDecrypt(e);
}
else if (e.Book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.NotLiberated)
{
SetQueueCollapseState(false);
processBookQueue1.AddDownloadPdf(e);
}
else if (e.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(e.Book.AudibleProductId);
if (!Go.To.File(filePath))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
MessageBox.Show($"File not found" + suffix);
}
}
}
private void SetQueueCollapseState(bool collapsed)
{
if (collapsed && !splitContainer1.Panel2Collapsed)
{ {
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth; WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
splitContainer1.Panel2.Controls.Remove(processBookQueue1); splitContainer1.Panel2.Controls.Remove(processBookQueue1);
splitContainer1.Panel2Collapsed = true; splitContainer1.Panel2Collapsed = true;
Width -= WidthChange; Width -= WidthChange;
hideQueueBtn.Text = "❱❱❱";
} }
else if (!collapsed && splitContainer1.Panel2Collapsed)
{
Width += WidthChange;
splitContainer1.Panel2.Controls.Add(processBookQueue1);
splitContainer1.Panel2Collapsed = false;
processBookQueue1.popoutBtn.Visible = true;
}
toggleQueueHideBtn.Text = splitContainer1.Panel2Collapsed ? "❰❰❰" : "❱❱❱";
}
private void ToggleQueueHideBtn_Click(object sender, EventArgs e)
{
SetQueueCollapseState(!splitContainer1.Panel2Collapsed);
Configuration.Instance.SetObject(nameof(splitContainer1.Panel2Collapsed), splitContainer1.Panel2Collapsed);
} }
private void ProcessBookQueue1_PopOut(object sender, EventArgs e) private void ProcessBookQueue1_PopOut(object sender, EventArgs e)
@ -50,8 +82,8 @@ namespace LibationWinForms
dockForm.PassControl(processBookQueue1); dockForm.PassControl(processBookQueue1);
dockForm.Show(); dockForm.Show();
this.Width -= dockForm.WidthChange; this.Width -= dockForm.WidthChange;
hideQueueBtn.Visible = false; toggleQueueHideBtn.Visible = false;
int deltax = filterBtn.Margin.Right + hideQueueBtn.Width + hideQueueBtn.Margin.Left; int deltax = filterBtn.Margin.Right + toggleQueueHideBtn.Width + toggleQueueHideBtn.Margin.Left;
filterBtn.Location= new System.Drawing.Point(filterBtn.Location.X + deltax, filterBtn.Location.Y); filterBtn.Location= new System.Drawing.Point(filterBtn.Location.X + deltax, filterBtn.Location.Y);
filterSearchTb.Location = new System.Drawing.Point(filterSearchTb.Location.X + deltax, filterSearchTb.Location.Y); filterSearchTb.Location = new System.Drawing.Point(filterSearchTb.Location.X + deltax, filterSearchTb.Location.Y);
} }
@ -66,8 +98,8 @@ namespace LibationWinForms
processBookQueue1.popoutBtn.Visible = true; processBookQueue1.popoutBtn.Visible = true;
dockForm.SaveSizeAndLocation(Configuration.Instance); dockForm.SaveSizeAndLocation(Configuration.Instance);
this.Focus(); this.Focus();
hideQueueBtn.Visible = true; toggleQueueHideBtn.Visible = true;
int deltax = filterBtn.Margin.Right + hideQueueBtn.Width + hideQueueBtn.Margin.Left; int deltax = filterBtn.Margin.Right + toggleQueueHideBtn.Width + toggleQueueHideBtn.Margin.Left;
filterBtn.Location = new System.Drawing.Point(filterBtn.Location.X - deltax, filterBtn.Location.Y); filterBtn.Location = new System.Drawing.Point(filterBtn.Location.X - deltax, filterBtn.Location.Y);
filterSearchTb.Location = new System.Drawing.Point(filterSearchTb.Location.X - deltax, filterSearchTb.Location.Y); filterSearchTb.Location = new System.Drawing.Point(filterSearchTb.Location.X - deltax, filterSearchTb.Location.Y);
} }

View File

@ -61,7 +61,10 @@ namespace LibationWinForms
} }
private async void liberateVisible(object sender, EventArgs e) private async void liberateVisible(object sender, EventArgs e)
=> await Task.Run(() => processBookQueue1.AddDownloadDecrypt(productsGrid.GetVisible())); {
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddDownloadDecrypt(productsGrid.GetVisible()));
}
private void replaceTagsToolStripMenuItem_Click(object sender, EventArgs e) private void replaceTagsToolStripMenuItem_Click(object sender, EventArgs e)
{ {

View File

@ -42,6 +42,8 @@ namespace LibationWinForms.ProcessQueue
public bool Running => !QueueRunner?.IsCompleted ?? false; public bool Running => !QueueRunner?.IsCompleted ?? false;
public ToolStripButton popoutBtn = new(); public ToolStripButton popoutBtn = new();
private System.Threading.SynchronizationContext syncContext { get; } = System.Threading.SynchronizationContext.Current;
public ProcessQueueControl() public ProcessQueueControl()
{ {
InitializeComponent(); InitializeComponent();
@ -122,12 +124,13 @@ namespace LibationWinForms.ProcessQueue
private void AddToQueue(ProcessBook pbook) private void AddToQueue(ProcessBook pbook)
{ {
BeginInvoke(() => syncContext.Post(_ =>
{ {
Queue.Enqueue(pbook); Queue.Enqueue(pbook);
if (!Running) if (!Running)
QueueRunner = QueueLoop(); QueueRunner = QueueLoop();
}); },
null);
} }
DateTime StartintTime; DateTime StartintTime;
@ -255,6 +258,7 @@ namespace LibationWinForms.ProcessQueue
/// Updates the display of a single <see cref="ProcessBookControl"/> at <paramref name="queueIndex"/> within <see cref="Queue"/> /// Updates the display of a single <see cref="ProcessBookControl"/> at <paramref name="queueIndex"/> within <see cref="Queue"/>
/// </summary> /// </summary>
/// <param name="queueIndex">index of the <see cref="ProcessBook"/> within the <see cref="Queue"/></param> /// <param name="queueIndex">index of the <see cref="ProcessBook"/> within the <see cref="Queue"/></param>
/// <param name="propertyName">The nme of the property that needs updating. If null, all properties are updated.</param>
private void UpdateControl(int queueIndex, string propertyName = null) private void UpdateControl(int queueIndex, string propertyName = null)
{ {
int i = queueIndex - FirstVisible; int i = queueIndex - FirstVisible;
@ -263,12 +267,12 @@ namespace LibationWinForms.ProcessQueue
var proc = Queue[queueIndex]; var proc = Queue[queueIndex];
Panels[i].Invoke(() => syncContext.Send(_ =>
{ {
Panels[i].SuspendLayout(); Panels[i].SuspendLayout();
if (propertyName is null || propertyName == nameof(proc.Cover)) if (propertyName is null or nameof(proc.Cover))
Panels[i].SetCover(proc.Cover); Panels[i].SetCover(proc.Cover);
if (propertyName is null || propertyName == nameof(proc.BookText)) if (propertyName is null or nameof(proc.BookText))
Panels[i].SetBookInfo(proc.BookText); Panels[i].SetBookInfo(proc.BookText);
if (proc.Result != ProcessBookResult.None) if (proc.Result != ProcessBookResult.None)
@ -277,14 +281,15 @@ namespace LibationWinForms.ProcessQueue
return; return;
} }
if (propertyName is null || propertyName == nameof(proc.Status)) if (propertyName is null or nameof(proc.Status))
Panels[i].SetStatus(proc.Status); Panels[i].SetStatus(proc.Status);
if (propertyName is null || propertyName == nameof(proc.Progress)) if (propertyName is null or nameof(proc.Progress))
Panels[i].SetProgrss(proc.Progress); Panels[i].SetProgrss(proc.Progress);
if (propertyName is null || propertyName == nameof(proc.TimeRemaining)) if (propertyName is null or nameof(proc.TimeRemaining))
Panels[i].SetRemainingTime(proc.TimeRemaining); Panels[i].SetRemainingTime(proc.TimeRemaining);
Panels[i].ResumeLayout(); Panels[i].ResumeLayout();
}); },
null);
} }
private void UpdateAllControls() private void UpdateAllControls()

View File

@ -17,6 +17,8 @@ namespace LibationWinForms
public SyncBindingSource(object dataSource, string dataMember) : base(dataSource, dataMember) public SyncBindingSource(object dataSource, string dataMember) : base(dataSource, dataMember)
=> syncContext = SynchronizationContext.Current; => syncContext = SynchronizationContext.Current;
public override bool SupportsFiltering => true;
protected override void OnListChanged(ListChangedEventArgs e) protected override void OnListChanged(ListChangedEventArgs e)
{ {
if (syncContext is not null) if (syncContext is not null)

View File

@ -0,0 +1,92 @@
using ApplicationServices;
using Dinah.Core.DataBinding;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms
{
/*
* Allows filtering of the underlying SortableBindingList<GridEntry>
* by implementing IBindingListView and using SearchEngineCommands
*
* When filtering is applied, the filtered-out items are removed
* from the base list and added to the private FilterRemoved list.
* When filtering is removed, items in the FilterRemoved list are
* added back to the base list.
*
* Remove is overridden to ensure that removed items are removed from
* the base list (visible items) as well as the FilterRemoved list.
*/
internal class FilterableSortableBindingList : SortableBindingList<GridEntry>, IBindingListView
{
/// <summary>
/// Items that were removed from the base list due to filtering
/// </summary>
private readonly List<GridEntry> FilterRemoved = new();
private string FilterString;
public FilterableSortableBindingList(IEnumerable<GridEntry> enumeration) : base(enumeration) { }
public bool SupportsFiltering => true;
public string Filter { get => FilterString; set => ApplyFilter(value); }
#region Unused - Advanced Filtering
public bool SupportsAdvancedSorting => false;
//This ApplySort overload is only called if SupportsAdvancedSorting is true.
//Otherwise BindingList.ApplySort() is used
public void ApplySort(ListSortDescriptionCollection sorts) => throw new NotImplementedException();
public ListSortDescriptionCollection SortDescriptions => throw new NotImplementedException();
#endregion
public new void Remove(GridEntry entry)
{
FilterRemoved.Remove(entry);
base.Remove(entry);
}
/// <returns>All items in the list, including those filtered out.</returns>
public List<GridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
private void ApplyFilter(string filterString)
{
if (filterString != FilterString)
RemoveFilter();
FilterString = filterString;
var searchResults = SearchEngineCommands.Search(filterString);
var filteredOut = Items.ExceptBy(searchResults.Docs.Select(d => d.ProductId), ge => ge.AudibleProductId);
for (int i = Items.Count - 1; i >= 0; i--)
{
if (filteredOut.Contains(Items[i]))
{
FilterRemoved.Add(Items[i]);
Items.RemoveAt(i);
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, i));
}
}
}
public void RemoveFilter()
{
if (FilterString is null) return;
for (int i = 0; i < FilterRemoved.Count; i++)
base.InsertItem(i, FilterRemoved[i]);
FilterRemoved.Clear();
if (IsSortedCore)
Sort();
else
//No user-defined sort is applied, so do default sorting by date added, descending
((List<GridEntry>)Items).Sort((i1, i2) => i2.LibraryBook.DateAdded.CompareTo(i1.LibraryBook.DateAdded));
FilterString = null;
}
}
}

View File

@ -160,10 +160,12 @@ namespace LibationWinForms
break; break;
case nameof(udi.BookStatus): case nameof(udi.BookStatus):
Book.UserDefinedItem.BookStatus = udi.BookStatus; Book.UserDefinedItem.BookStatus = udi.BookStatus;
_bookStatus = udi.BookStatus;
NotifyPropertyChanged(nameof(Liberate)); NotifyPropertyChanged(nameof(Liberate));
break; break;
case nameof(udi.PdfStatus): case nameof(udi.PdfStatus):
Book.UserDefinedItem.PdfStatus = udi.PdfStatus; Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
_pdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate)); NotifyPropertyChanged(nameof(Liberate));
break; break;
} }

View File

@ -6,9 +6,6 @@ using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using ApplicationServices; using ApplicationServices;
using DataLayer; using DataLayer;
using Dinah.Core;
using Dinah.Core.DataBinding;
using Dinah.Core.Threading;
using Dinah.Core.Windows.Forms; using Dinah.Core.Windows.Forms;
using FileLiberator; using FileLiberator;
using LibationFileManager; using LibationFileManager;
@ -55,8 +52,6 @@ namespace LibationWinForms
EnableDoubleBuffering(); EnableDoubleBuffering();
// sorting breaks filters. must reapply filters after sorting
_dataGridView.Sorted += reapplyFilter;
_dataGridView.CellContentClick += DataGridView_CellContentClick; _dataGridView.CellContentClick += DataGridView_CellContentClick;
this.Load += ProductsGrid_Load; this.Load += ProductsGrid_Load;
@ -132,20 +127,6 @@ namespace LibationWinForms
private void Liberate_Click(GridEntry liveGridEntry) private void Liberate_Click(GridEntry liveGridEntry)
{ {
var libraryBook = liveGridEntry.LibraryBook;
// liberated: open explorer to file
if (libraryBook.Book.Audio_Exists())
{
var filePath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
MessageBox.Show($"File not found" + suffix);
}
return;
}
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook); LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
} }
@ -160,7 +141,7 @@ namespace LibationWinForms
#region UI display functions #region UI display functions
private SortableBindingList<GridEntry> bindingList; private FilterableSortableBindingList bindingList;
private bool hasBeenDisplayed; private bool hasBeenDisplayed;
public event EventHandler InitialLoaded; public event EventHandler InitialLoaded;
@ -169,124 +150,91 @@ namespace LibationWinForms
// don't return early if lib size == 0. this will not update correctly if all books are removed // don't return early if lib size == 0. this will not update correctly if all books are removed
var lib = DbContexts.GetLibrary_Flat_NoTracking(); var lib = DbContexts.GetLibrary_Flat_NoTracking();
var orderedBooks = lib
// default load order
.OrderByDescending(lb => lb.DateAdded)
//// more advanced example: sort by author, then series, then title
//.OrderBy(lb => lb.Book.AuthorNames)
// .ThenBy(lb => lb.Book.SeriesSortable)
// .ThenBy(lb => lb.Book.TitleSortable)
.ToList();
// bind
if (bindingList?.Count > 0)
updateGrid(orderedBooks);
else
bindToGrid(orderedBooks);
// re-apply previous filter
reapplyFilter();
if (!hasBeenDisplayed) if (!hasBeenDisplayed)
{ {
// bind
bindToGrid(lib);
hasBeenDisplayed = true; hasBeenDisplayed = true;
InitialLoaded?.Invoke(this, new()); InitialLoaded?.Invoke(this, new());
VisibleCountChanged?.Invoke(this, bindingList.Count);
} }
else
updateGrid(lib);
} }
private void bindToGrid(List<DataLayer.LibraryBook> orderedBooks) private void bindToGrid(List<LibraryBook> dbBooks)
{ {
bindingList = new SortableBindingList<GridEntry>(orderedBooks.Select(lb => toGridEntry(lb))); bindingList = new FilterableSortableBindingList(dbBooks.OrderByDescending(lb => lb.DateAdded).Select(lb => new GridEntry(lb)));
gridEntryBindingSource.DataSource = bindingList; gridEntryBindingSource.DataSource = bindingList;
} }
private void updateGrid(List<DataLayer.LibraryBook> orderedBooks) private void updateGrid(List<LibraryBook> dbBooks)
{ {
for (var i = orderedBooks.Count - 1; i >= 0; i--) int visibleCount = bindingList.Count;
string existingFilter = gridEntryBindingSource.Filter;
//Add absent books to grid, or update current books
var allItmes = bindingList.AllItems();
for (var i = dbBooks.Count - 1; i >= 0; i--)
{ {
var libraryBook = orderedBooks[i]; var libraryBook = dbBooks[i];
var existingItem = bindingList.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId); var existingItem = allItmes.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId);
// add new to top // add new to top
if (existingItem is null) if (existingItem is null)
bindingList.Insert(0, toGridEntry(libraryBook)); bindingList.Insert(0, new GridEntry(libraryBook));
// update existing // update existing
else else
existingItem.UpdateLibraryBook(libraryBook); existingItem.UpdateLibraryBook(libraryBook);
} }
// remove deleted from grid. note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this if (bindingList.Count != visibleCount)
var oldIds = bindingList.Select(ge => ge.AudibleProductId).ToList();
var newIds = orderedBooks.Select(lb => lb.Book.AudibleProductId).ToList();
var remove = oldIds.Except(newIds).ToList();
foreach (var id in remove)
{ {
var oldItem = bindingList.FirstOrDefault(ge => ge.AudibleProductId == id); //re-filter for newly added items
if (oldItem is not null) Filter(null);
bindingList.Remove(oldItem); Filter(existingFilter);
}
} }
private GridEntry toGridEntry(DataLayer.LibraryBook libraryBook) // remove deleted from grid.
{ // note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
var entry = new GridEntry(libraryBook); var removedBooks =
entry.Committed += reapplyFilter; bindingList
// see also notes in Libation/Source/__ARCHITECTURE NOTES.txt :: MVVM .AllItems()
entry.LibraryBookUpdated += (sender, _) => _dataGridView.InvalidateRow(_dataGridView.GetRowIdOfBoundItem((GridEntry)sender)); .ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId)
return entry; .ToList();
foreach (var removed in removedBooks)
//no need to re-filter for removed books
bindingList.Remove(removed);
if (bindingList.Count != visibleCount)
VisibleCountChanged?.Invoke(this, bindingList.Count);
} }
#endregion #endregion
#region Filter #region Filter
private string _filterSearchString;
private void reapplyFilter(object _ = null, EventArgs __ = null) => Filter(_filterSearchString);
public void Filter(string searchString) public void Filter(string searchString)
{ {
// empty string is valid. null is not int visibleCount = bindingList.Count;
if (searchString is null)
return;
_filterSearchString = searchString; if (string.IsNullOrEmpty(searchString))
gridEntryBindingSource.RemoveFilter();
else
gridEntryBindingSource.Filter = searchString;
if (_dataGridView.Rows.Count == 0) if (visibleCount != bindingList.Count)
return; VisibleCountChanged?.Invoke(this, bindingList.Count);
var initVisible = getVisible().Count();
var searchResults = SearchEngineCommands.Search(searchString);
var productIds = searchResults.Docs.Select(d => d.ProductId).ToList();
// https://stackoverflow.com/a/18942430
var bindingContext = BindingContext[_dataGridView.DataSource];
bindingContext.SuspendBinding();
{
this.UIThreadSync(() =>
{
for (var r = _dataGridView.RowCount - 1; r >= 0; r--)
_dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
});
}
// Causes repainting of the DataGridView
bindingContext.ResumeBinding();
var endVisible = getVisible().Count();
if (initVisible != endVisible)
VisibleCountChanged?.Invoke(this, endVisible);
} }
#endregion #endregion
private IEnumerable<DataGridViewRow> getVisible() internal List<LibraryBook> GetVisible()
=> _dataGridView => bindingList
.AsEnumerable() .Select(row => row.LibraryBook)
.Where(row => row.Visible);
internal List<DataLayer.LibraryBook> GetVisible()
=> getVisible()
.Select(row => ((GridEntry)row.DataBoundItem).LibraryBook)
.ToList(); .ToList();
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex); private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);