Merge pull request #249 from Mbucari/master
Add FilterableSortableBindingList to handle filtering the DataGridView
This commit is contained in:
commit
a89b07394f
@ -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
|
||||
}
|
||||
}
|
||||
@ -2,30 +2,48 @@
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.Threading;
|
||||
using LibationWinForms.BookLiberation.BaseForms;
|
||||
using FileLiberator;
|
||||
|
||||
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()
|
||||
{
|
||||
//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();
|
||||
|
||||
this.SetLibationIcon();
|
||||
progressLbl.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
|
||||
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);
|
||||
}
|
||||
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
|
||||
if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
|
||||
return;
|
||||
|
||||
24
Source/LibationWinForms/Form1.Designer.cs
generated
24
Source/LibationWinForms/Form1.Designer.cs
generated
@ -73,7 +73,7 @@
|
||||
this.addQuickFilterBtn = new System.Windows.Forms.Button();
|
||||
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
|
||||
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.menuStrip1.SuspendLayout();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
@ -462,7 +462,7 @@
|
||||
// panel1
|
||||
//
|
||||
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.addQuickFilterBtn);
|
||||
this.panel1.Controls.Add(this.filterHelpBtn);
|
||||
@ -477,15 +477,15 @@
|
||||
//
|
||||
// hideQueueBtn
|
||||
//
|
||||
this.hideQueueBtn.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.hideQueueBtn.Margin = new System.Windows.Forms.Padding(5, 4, 17, 4);
|
||||
this.hideQueueBtn.Name = "hideQueueBtn";
|
||||
this.hideQueueBtn.Size = new System.Drawing.Size(38, 36);
|
||||
this.hideQueueBtn.TabIndex = 8;
|
||||
this.hideQueueBtn.Text = "❰❰❰";
|
||||
this.hideQueueBtn.UseVisualStyleBackColor = true;
|
||||
this.hideQueueBtn.Click += new System.EventHandler(this.HideQueueBtn_Click);
|
||||
this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.toggleQueueHideBtn.Location = new System.Drawing.Point(966, 4);
|
||||
this.toggleQueueHideBtn.Margin = new System.Windows.Forms.Padding(5, 4, 17, 4);
|
||||
this.toggleQueueHideBtn.Name = "hideQueueBtn";
|
||||
this.toggleQueueHideBtn.Size = new System.Drawing.Size(38, 36);
|
||||
this.toggleQueueHideBtn.TabIndex = 8;
|
||||
this.toggleQueueHideBtn.Text = "❱❱❱";
|
||||
this.toggleQueueHideBtn.UseVisualStyleBackColor = true;
|
||||
this.toggleQueueHideBtn.Click += new System.EventHandler(this.ToggleQueueHideBtn_Click);
|
||||
//
|
||||
// processBookQueue1
|
||||
//
|
||||
@ -571,6 +571,6 @@
|
||||
private System.Windows.Forms.SplitContainer splitContainer1;
|
||||
private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1;
|
||||
private System.Windows.Forms.Panel panel1;
|
||||
private System.Windows.Forms.Button hideQueueBtn;
|
||||
private System.Windows.Forms.Button toggleQueueHideBtn;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,12 +11,18 @@ namespace LibationWinForms
|
||||
|
||||
//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)
|
||||
=> 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)));
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
@ -29,8 +35,11 @@ namespace LibationWinForms
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Warning);
|
||||
if (result == DialogResult.Yes)
|
||||
{
|
||||
SetQueueCollapseState(false);
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,41 +1,73 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.ProcessQueue;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
private void Configure_ProcessQueue()
|
||||
{
|
||||
productsGrid.LiberateClicked += (_, lb) => processBookQueue1.AddDownloadDecrypt(lb);
|
||||
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
|
||||
}
|
||||
|
||||
int WidthChange = 0;
|
||||
private void HideQueueBtn_Click(object sender, EventArgs e)
|
||||
private void Configure_ProcessQueue()
|
||||
{
|
||||
if (splitContainer1.Panel2Collapsed)
|
||||
{
|
||||
WidthChange = WidthChange == 0 ? splitContainer1.Panel2.Width + splitContainer1.SplitterWidth : WidthChange;
|
||||
Width += WidthChange;
|
||||
splitContainer1.Panel2.Controls.Add(processBookQueue1);
|
||||
splitContainer1.Panel2Collapsed = false;
|
||||
processBookQueue1.popoutBtn.Visible = true;
|
||||
hideQueueBtn.Text = "❰❰❰";
|
||||
productsGrid.LiberateClicked += ProductsGrid_LiberateClicked;
|
||||
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
|
||||
var coppalseState = Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.Panel2Collapsed));
|
||||
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
|
||||
SetQueueCollapseState(coppalseState);
|
||||
}
|
||||
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;
|
||||
splitContainer1.Panel2.Controls.Remove(processBookQueue1);
|
||||
splitContainer1.Panel2Collapsed = true;
|
||||
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)
|
||||
@ -50,8 +82,8 @@ namespace LibationWinForms
|
||||
dockForm.PassControl(processBookQueue1);
|
||||
dockForm.Show();
|
||||
this.Width -= dockForm.WidthChange;
|
||||
hideQueueBtn.Visible = false;
|
||||
int deltax = filterBtn.Margin.Right + hideQueueBtn.Width + hideQueueBtn.Margin.Left;
|
||||
toggleQueueHideBtn.Visible = false;
|
||||
int deltax = filterBtn.Margin.Right + toggleQueueHideBtn.Width + toggleQueueHideBtn.Margin.Left;
|
||||
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);
|
||||
}
|
||||
@ -66,8 +98,8 @@ namespace LibationWinForms
|
||||
processBookQueue1.popoutBtn.Visible = true;
|
||||
dockForm.SaveSizeAndLocation(Configuration.Instance);
|
||||
this.Focus();
|
||||
hideQueueBtn.Visible = true;
|
||||
int deltax = filterBtn.Margin.Right + hideQueueBtn.Width + hideQueueBtn.Margin.Left;
|
||||
toggleQueueHideBtn.Visible = true;
|
||||
int deltax = filterBtn.Margin.Right + toggleQueueHideBtn.Width + toggleQueueHideBtn.Margin.Left;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -61,7 +61,10 @@ namespace LibationWinForms
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@ -42,6 +42,8 @@ namespace LibationWinForms.ProcessQueue
|
||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||
public ToolStripButton popoutBtn = new();
|
||||
|
||||
private System.Threading.SynchronizationContext syncContext { get; } = System.Threading.SynchronizationContext.Current;
|
||||
|
||||
public ProcessQueueControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
@ -122,12 +124,13 @@ namespace LibationWinForms.ProcessQueue
|
||||
|
||||
private void AddToQueue(ProcessBook pbook)
|
||||
{
|
||||
BeginInvoke(() =>
|
||||
syncContext.Post(_ =>
|
||||
{
|
||||
Queue.Enqueue(pbook);
|
||||
if (!Running)
|
||||
QueueRunner = QueueLoop();
|
||||
});
|
||||
},
|
||||
null);
|
||||
}
|
||||
|
||||
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"/>
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
int i = queueIndex - FirstVisible;
|
||||
@ -263,12 +267,12 @@ namespace LibationWinForms.ProcessQueue
|
||||
|
||||
var proc = Queue[queueIndex];
|
||||
|
||||
Panels[i].Invoke(() =>
|
||||
syncContext.Send(_ =>
|
||||
{
|
||||
Panels[i].SuspendLayout();
|
||||
if (propertyName is null || propertyName == nameof(proc.Cover))
|
||||
if (propertyName is null or nameof(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);
|
||||
|
||||
if (proc.Result != ProcessBookResult.None)
|
||||
@ -277,14 +281,15 @@ namespace LibationWinForms.ProcessQueue
|
||||
return;
|
||||
}
|
||||
|
||||
if (propertyName is null || propertyName == nameof(proc.Status))
|
||||
if (propertyName is null or nameof(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);
|
||||
if (propertyName is null || propertyName == nameof(proc.TimeRemaining))
|
||||
if (propertyName is null or nameof(proc.TimeRemaining))
|
||||
Panels[i].SetRemainingTime(proc.TimeRemaining);
|
||||
Panels[i].ResumeLayout();
|
||||
});
|
||||
},
|
||||
null);
|
||||
}
|
||||
|
||||
private void UpdateAllControls()
|
||||
|
||||
@ -17,6 +17,8 @@ namespace LibationWinForms
|
||||
public SyncBindingSource(object dataSource, string dataMember) : base(dataSource, dataMember)
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
|
||||
public override bool SupportsFiltering => true;
|
||||
|
||||
protected override void OnListChanged(ListChangedEventArgs e)
|
||||
{
|
||||
if (syncContext is not null)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -160,10 +160,12 @@ namespace LibationWinForms
|
||||
break;
|
||||
case nameof(udi.BookStatus):
|
||||
Book.UserDefinedItem.BookStatus = udi.BookStatus;
|
||||
_bookStatus = udi.BookStatus;
|
||||
NotifyPropertyChanged(nameof(Liberate));
|
||||
break;
|
||||
case nameof(udi.PdfStatus):
|
||||
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
|
||||
_pdfStatus = udi.PdfStatus;
|
||||
NotifyPropertyChanged(nameof(Liberate));
|
||||
break;
|
||||
}
|
||||
|
||||
@ -6,9 +6,6 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.DataBinding;
|
||||
using Dinah.Core.Threading;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
@ -55,8 +52,6 @@ namespace LibationWinForms
|
||||
|
||||
EnableDoubleBuffering();
|
||||
|
||||
// sorting breaks filters. must reapply filters after sorting
|
||||
_dataGridView.Sorted += reapplyFilter;
|
||||
_dataGridView.CellContentClick += DataGridView_CellContentClick;
|
||||
|
||||
this.Load += ProductsGrid_Load;
|
||||
@ -132,20 +127,6 @@ namespace LibationWinForms
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -160,7 +141,7 @@ namespace LibationWinForms
|
||||
|
||||
#region UI display functions
|
||||
|
||||
private SortableBindingList<GridEntry> bindingList;
|
||||
private FilterableSortableBindingList bindingList;
|
||||
|
||||
private bool hasBeenDisplayed;
|
||||
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
|
||||
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)
|
||||
{
|
||||
// bind
|
||||
bindToGrid(lib);
|
||||
hasBeenDisplayed = true;
|
||||
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;
|
||||
}
|
||||
|
||||
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 existingItem = bindingList.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||
var libraryBook = dbBooks[i];
|
||||
var existingItem = allItmes.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||
|
||||
// add new to top
|
||||
if (existingItem is null)
|
||||
bindingList.Insert(0, toGridEntry(libraryBook));
|
||||
bindingList.Insert(0, new GridEntry(libraryBook));
|
||||
// update existing
|
||||
else
|
||||
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
|
||||
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)
|
||||
if (bindingList.Count != visibleCount)
|
||||
{
|
||||
var oldItem = bindingList.FirstOrDefault(ge => ge.AudibleProductId == id);
|
||||
if (oldItem is not null)
|
||||
bindingList.Remove(oldItem);
|
||||
}
|
||||
//re-filter for newly added items
|
||||
Filter(null);
|
||||
Filter(existingFilter);
|
||||
}
|
||||
|
||||
private GridEntry toGridEntry(DataLayer.LibraryBook libraryBook)
|
||||
{
|
||||
var entry = new GridEntry(libraryBook);
|
||||
entry.Committed += reapplyFilter;
|
||||
// see also notes in Libation/Source/__ARCHITECTURE NOTES.txt :: MVVM
|
||||
entry.LibraryBookUpdated += (sender, _) => _dataGridView.InvalidateRow(_dataGridView.GetRowIdOfBoundItem((GridEntry)sender));
|
||||
return entry;
|
||||
// remove deleted from grid.
|
||||
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
||||
var removedBooks =
|
||||
bindingList
|
||||
.AllItems()
|
||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId)
|
||||
.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
|
||||
|
||||
#region Filter
|
||||
|
||||
private string _filterSearchString;
|
||||
private void reapplyFilter(object _ = null, EventArgs __ = null) => Filter(_filterSearchString);
|
||||
public void Filter(string searchString)
|
||||
{
|
||||
// empty string is valid. null is not
|
||||
if (searchString is null)
|
||||
return;
|
||||
int visibleCount = bindingList.Count;
|
||||
|
||||
_filterSearchString = searchString;
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
gridEntryBindingSource.RemoveFilter();
|
||||
else
|
||||
gridEntryBindingSource.Filter = searchString;
|
||||
|
||||
if (_dataGridView.Rows.Count == 0)
|
||||
return;
|
||||
|
||||
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);
|
||||
if (visibleCount != bindingList.Count)
|
||||
VisibleCountChanged?.Invoke(this, bindingList.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private IEnumerable<DataGridViewRow> getVisible()
|
||||
=> _dataGridView
|
||||
.AsEnumerable()
|
||||
.Where(row => row.Visible);
|
||||
|
||||
internal List<DataLayer.LibraryBook> GetVisible()
|
||||
=> getVisible()
|
||||
.Select(row => ((GridEntry)row.DataBoundItem).LibraryBook)
|
||||
internal List<LibraryBook> GetVisible()
|
||||
=> bindingList
|
||||
.Select(row => row.LibraryBook)
|
||||
.ToList();
|
||||
|
||||
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user