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);