diff --git a/Source/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs b/Source/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs
deleted file mode 100644
index 03f5ba05..00000000
--- a/Source/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs
+++ /dev/null
@@ -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
-
- ///
- /// If the form was shown using Show (not ShowDialog), Form.Close calls Form.Dispose
- ///
- private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThreadAsync(Close);
- private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThreadAsync(Dispose);
-
- ///
- /// 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.
- ///
- 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 setCoverArtDelegate) { }
- #endregion
- }
-}
diff --git a/Source/LibationWinForms/BookLiberation/DownloadForm.cs b/Source/LibationWinForms/BookLiberation/DownloadForm.cs
index 3a307476..77d6058f 100644
--- a/Source/LibationWinForms/BookLiberation/DownloadForm.cs
+++ b/Source/LibationWinForms/BookLiberation/DownloadForm.cs
@@ -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;
diff --git a/Source/LibationWinForms/Form1.Designer.cs b/Source/LibationWinForms/Form1.Designer.cs
index b6be6147..b4cf9508 100644
--- a/Source/LibationWinForms/Form1.Designer.cs
+++ b/Source/LibationWinForms/Form1.Designer.cs
@@ -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;
}
}
diff --git a/Source/LibationWinForms/Form1.Liberate.cs b/Source/LibationWinForms/Form1.Liberate.cs
index d1369d7a..66a24ebf 100644
--- a/Source/LibationWinForms/Form1.Liberate.cs
+++ b/Source/LibationWinForms/Form1.Liberate.cs
@@ -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()
- .Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated)));
+ {
+ 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.
}
}
diff --git a/Source/LibationWinForms/Form1.ProcessQueue.cs b/Source/LibationWinForms/Form1.ProcessQueue.cs
index acf4ac66..74d30406 100644
--- a/Source/LibationWinForms/Form1.ProcessQueue.cs
+++ b/Source/LibationWinForms/Form1.ProcessQueue.cs
@@ -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)
+ productsGrid.LiberateClicked += ProductsGrid_LiberateClicked;
+ processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
+ var coppalseState = Configuration.Instance.GetNonString(nameof(splitContainer1.Panel2Collapsed));
+ WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
+ SetQueueCollapseState(coppalseState);
+ }
+
+ private void ProductsGrid_LiberateClicked(object sender, LibraryBook e)
+ {
+ if (e.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
{
- 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 = "❰❰❰";
+ SetQueueCollapseState(false);
+ processBookQueue1.AddDownloadDecrypt(e);
}
- else
+ 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);
}
diff --git a/Source/LibationWinForms/Form1.VisibleBooks.cs b/Source/LibationWinForms/Form1.VisibleBooks.cs
index 0890ddb3..65dfecca 100644
--- a/Source/LibationWinForms/Form1.VisibleBooks.cs
+++ b/Source/LibationWinForms/Form1.VisibleBooks.cs
@@ -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)
{
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
index b0fa7aa3..98b7c01a 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
@@ -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 at within
///
/// index of the within the
+ /// The nme of the property that needs updating. If null, all properties are updated.
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()
diff --git a/Source/LibationWinForms/SyncBindingSource.cs b/Source/LibationWinForms/SyncBindingSource.cs
index 49c65eb3..42d38ad9 100644
--- a/Source/LibationWinForms/SyncBindingSource.cs
+++ b/Source/LibationWinForms/SyncBindingSource.cs
@@ -17,7 +17,9 @@ namespace LibationWinForms
public SyncBindingSource(object dataSource, string dataMember) : base(dataSource, dataMember)
=> syncContext = SynchronizationContext.Current;
- protected override void OnListChanged(ListChangedEventArgs e)
+ public override bool SupportsFiltering => true;
+
+ protected override void OnListChanged(ListChangedEventArgs e)
{
if (syncContext is not null)
syncContext.Send(_ => base.OnListChanged(e), null);
diff --git a/Source/LibationWinForms/grid/FilterableSortableBindingList.cs b/Source/LibationWinForms/grid/FilterableSortableBindingList.cs
new file mode 100644
index 00000000..5cee30dd
--- /dev/null
+++ b/Source/LibationWinForms/grid/FilterableSortableBindingList.cs
@@ -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
+ * 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, IBindingListView
+ {
+ ///
+ /// Items that were removed from the base list due to filtering
+ ///
+ private readonly List FilterRemoved = new();
+ private string FilterString;
+ public FilterableSortableBindingList(IEnumerable 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);
+ }
+
+ /// All items in the list, including those filtered out.
+ public List 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)Items).Sort((i1, i2) => i2.LibraryBook.DateAdded.CompareTo(i1.LibraryBook.DateAdded));
+
+ FilterString = null;
+ }
+ }
+}
diff --git a/Source/LibationWinForms/grid/GridEntry.cs b/Source/LibationWinForms/grid/GridEntry.cs
index 7040316c..07ecb838 100644
--- a/Source/LibationWinForms/grid/GridEntry.cs
+++ b/Source/LibationWinForms/grid/GridEntry.cs
@@ -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;
}
diff --git a/Source/LibationWinForms/grid/ProductsGrid.cs b/Source/LibationWinForms/grid/ProductsGrid.cs
index 0270a1e2..690cb00d 100644
--- a/Source/LibationWinForms/grid/ProductsGrid.cs
+++ b/Source/LibationWinForms/grid/ProductsGrid.cs
@@ -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 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 orderedBooks)
+ private void bindToGrid(List dbBooks)
{
- bindingList = new SortableBindingList(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 orderedBooks)
+ private void updateGrid(List 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 getVisible()
- => _dataGridView
- .AsEnumerable()
- .Where(row => row.Visible);
-
- internal List GetVisible()
- => getVisible()
- .Select(row => ((GridEntry)row.DataBoundItem).LibraryBook)
+ internal List GetVisible()
+ => bindingList
+ .Select(row => row.LibraryBook)
.ToList();
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem(rowIndex);