From 0e46cdb51458d6fb94f70e39824177c80aad2349 Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Fri, 13 May 2022 13:39:49 -0400 Subject: [PATCH] refactor Form1. too much in 1 file --- Source/LibationFileManager/QuickFilters.cs | 6 + Source/LibationWinForms/Form1.BackupCounts.cs | 109 +++ Source/LibationWinForms/Form1.Designer.cs | 28 +- Source/LibationWinForms/Form1.Export.cs | 47 ++ Source/LibationWinForms/Form1.Filter.cs | 45 ++ Source/LibationWinForms/Form1.Liberate.cs | 30 + .../LibationWinForms/Form1.PictureStorage.cs | 17 + Source/LibationWinForms/Form1.ProcessQueue.cs | 46 ++ Source/LibationWinForms/Form1.QuickFilters.cs | 60 ++ Source/LibationWinForms/Form1.ScanAuto.cs | 86 +++ Source/LibationWinForms/Form1.ScanManual.cs | 135 ++++ .../Form1.ScanNotification.cs | 37 + Source/LibationWinForms/Form1.Settings.cs | 18 + Source/LibationWinForms/Form1.VisibleBooks.cs | 128 ++++ Source/LibationWinForms/Form1.cs | 686 +----------------- .../LibationWinForms/LibationWinForms.csproj | 6 + Source/LibationWinForms/grid/ProductsGrid.cs | 34 +- 17 files changed, 850 insertions(+), 668 deletions(-) create mode 100644 Source/LibationWinForms/Form1.BackupCounts.cs create mode 100644 Source/LibationWinForms/Form1.Export.cs create mode 100644 Source/LibationWinForms/Form1.Filter.cs create mode 100644 Source/LibationWinForms/Form1.Liberate.cs create mode 100644 Source/LibationWinForms/Form1.PictureStorage.cs create mode 100644 Source/LibationWinForms/Form1.ProcessQueue.cs create mode 100644 Source/LibationWinForms/Form1.QuickFilters.cs create mode 100644 Source/LibationWinForms/Form1.ScanAuto.cs create mode 100644 Source/LibationWinForms/Form1.ScanManual.cs create mode 100644 Source/LibationWinForms/Form1.ScanNotification.cs create mode 100644 Source/LibationWinForms/Form1.Settings.cs create mode 100644 Source/LibationWinForms/Form1.VisibleBooks.cs diff --git a/Source/LibationFileManager/QuickFilters.cs b/Source/LibationFileManager/QuickFilters.cs index 9631070d..497f6e58 100644 --- a/Source/LibationFileManager/QuickFilters.cs +++ b/Source/LibationFileManager/QuickFilters.cs @@ -28,16 +28,22 @@ namespace LibationFileManager inMemoryState = JsonConvert.DeserializeObject(File.ReadAllText(JsonFile)); } + public static event EventHandler UseDefaultChanged; public static bool UseDefault { get => inMemoryState.UseDefault; set { + if (UseDefault == value) + return; + lock (locker) { inMemoryState.UseDefault = value; save(false); } + + UseDefaultChanged?.Invoke(null, null); } } diff --git a/Source/LibationWinForms/Form1.BackupCounts.cs b/Source/LibationWinForms/Form1.BackupCounts.cs new file mode 100644 index 00000000..4885bafb --- /dev/null +++ b/Source/LibationWinForms/Form1.BackupCounts.cs @@ -0,0 +1,109 @@ +using ApplicationServices; +using Dinah.Core; +using Dinah.Core.Threading; + +namespace LibationWinForms +{ + public partial class Form1 + { + private string beginBookBackupsToolStripMenuItem_format; + private string beginPdfBackupsToolStripMenuItem_format; + + protected void Configure_BackupCounts() + { + // back up string formats + beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text; + beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text; + + Load += setBackupCounts; + LibraryCommands.LibrarySizeChanged += setBackupCounts; + LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts; + } + + private System.ComponentModel.BackgroundWorker updateCountsBw; + private bool runBackupCountsAgain; + + private void setBackupCounts(object _, object __) + { + runBackupCountsAgain = true; + + if (updateCountsBw is not null) + return; + + updateCountsBw = new System.ComponentModel.BackgroundWorker(); + updateCountsBw.DoWork += UpdateCountsBw_DoWork; + updateCountsBw.RunWorkerCompleted += UpdateCountsBw_RunWorkerCompleted; + updateCountsBw.RunWorkerAsync(); + } + + private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e) + { + while (runBackupCountsAgain) + { + runBackupCountsAgain = false; + + var libraryStats = LibraryCommands.GetCounts(); + e.Result = libraryStats; + } + updateCountsBw = null; + } + + private void UpdateCountsBw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) + { + var libraryStats = e.Result as LibraryCommands.LibraryStats; + + setBookBackupCounts(libraryStats); + setPdfBackupCounts(libraryStats); + } + + private void setBookBackupCounts(LibraryCommands.LibraryStats libraryStats) + { + var backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}"; + + // enable/disable export + var hasResults = 0 < (libraryStats.booksFullyBackedUp + libraryStats.booksDownloadedOnly + libraryStats.booksNoProgress + libraryStats.booksError); + exportLibraryToolStripMenuItem.Enabled = hasResults; + + // update bottom numbers + var pending = libraryStats.booksNoProgress + libraryStats.booksDownloadedOnly; + var statusStripText + = !hasResults ? "No books. Begin by importing your library" + : libraryStats.booksError > 0 ? string.Format(backupsCountsLbl_Format + " Errors: {3}", libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp, libraryStats.booksError) + : pending > 0 ? string.Format(backupsCountsLbl_Format, libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp) + : $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up"; + + // update menu item + var menuItemText + = pending > 0 + ? $"{pending} remaining" + : "All books have been liberated"; + + // update UI + statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText); + menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0); + menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText)); + } + private void setPdfBackupCounts(LibraryCommands.LibraryStats libraryStats) + { + var pdfsCountsLbl_Format = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}"; + + // update bottom numbers + var hasResults = 0 < (libraryStats.pdfsNotDownloaded + libraryStats.pdfsDownloaded); + var statusStripText + = !hasResults ? "" + : libraryStats.pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded) + : $"| All {libraryStats.pdfsDownloaded} PDFs downloaded"; + + // update menu item + var menuItemText + = libraryStats.pdfsNotDownloaded > 0 + ? $"{libraryStats.pdfsNotDownloaded} remaining" + : "All PDFs have been downloaded"; + + // update UI + statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText); + menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0); + menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText)); + } + } +} diff --git a/Source/LibationWinForms/Form1.Designer.cs b/Source/LibationWinForms/Form1.Designer.cs index 162599e7..b54990b3 100644 --- a/Source/LibationWinForms/Form1.Designer.cs +++ b/Source/LibationWinForms/Form1.Designer.cs @@ -70,7 +70,7 @@ this.springLbl = new System.Windows.Forms.ToolStripStatusLabel(); this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel(); this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel(); - this.addFilterBtn = new System.Windows.Forms.Button(); + this.addQuickFilterBtn = new System.Windows.Forms.Button(); this.splitContainer1 = new System.Windows.Forms.SplitContainer(); this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessBookQueue(); this.menuStrip1.SuspendLayout(); @@ -285,14 +285,14 @@ this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem"; this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(256, 22); this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default"; - this.firstFilterIsDefaultToolStripMenuItem.Click += new System.EventHandler(this.FirstFilterIsDefaultToolStripMenuItem_Click); + this.firstFilterIsDefaultToolStripMenuItem.Click += new System.EventHandler(this.firstFilterIsDefaultToolStripMenuItem_Click); // // editQuickFiltersToolStripMenuItem // this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem"; this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22); this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters..."; - this.editQuickFiltersToolStripMenuItem.Click += new System.EventHandler(this.EditQuickFiltersToolStripMenuItem_Click); + this.editQuickFiltersToolStripMenuItem.Click += new System.EventHandler(this.editQuickFiltersToolStripMenuItem_Click); // // toolStripSeparator1 // @@ -424,16 +424,16 @@ this.pdfsCountsLbl.Size = new System.Drawing.Size(171, 17); this.pdfsCountsLbl.Text = "| [Calculating backed up PDFs]"; // - // addFilterBtn + // addQuickFilterBtn // - this.addFilterBtn.Location = new System.Drawing.Point(49, 27); - this.addFilterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - this.addFilterBtn.Name = "addFilterBtn"; - this.addFilterBtn.Size = new System.Drawing.Size(163, 27); - this.addFilterBtn.TabIndex = 4; - this.addFilterBtn.Text = "Add To Quick Filters"; - this.addFilterBtn.UseVisualStyleBackColor = true; - this.addFilterBtn.Click += new System.EventHandler(this.AddFilterBtn_Click); + this.addQuickFilterBtn.Location = new System.Drawing.Point(49, 27); + this.addQuickFilterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.addQuickFilterBtn.Name = "addQuickFilterBtn"; + this.addQuickFilterBtn.Size = new System.Drawing.Size(163, 27); + this.addQuickFilterBtn.TabIndex = 4; + this.addQuickFilterBtn.Text = "Add To Quick Filters"; + this.addQuickFilterBtn.UseVisualStyleBackColor = true; + this.addQuickFilterBtn.Click += new System.EventHandler(this.addQuickFilterBtn_Click); // // splitContainer1 // @@ -446,7 +446,7 @@ this.splitContainer1.Panel1.Controls.Add(this.menuStrip1); this.splitContainer1.Panel1.Controls.Add(this.gridPanel); this.splitContainer1.Panel1.Controls.Add(this.filterSearchTb); - this.splitContainer1.Panel1.Controls.Add(this.addFilterBtn); + this.splitContainer1.Panel1.Controls.Add(this.addQuickFilterBtn); this.splitContainer1.Panel1.Controls.Add(this.filterBtn); this.splitContainer1.Panel1.Controls.Add(this.statusStrip1); this.splitContainer1.Panel1.Controls.Add(this.filterHelpBtn); @@ -513,7 +513,7 @@ private System.Windows.Forms.ToolStripMenuItem scanLibraryToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem quickFiltersToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem firstFilterIsDefaultToolStripMenuItem; - private System.Windows.Forms.Button addFilterBtn; + private System.Windows.Forms.Button addQuickFilterBtn; private System.Windows.Forms.ToolStripMenuItem editQuickFiltersToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripSeparator1; private System.Windows.Forms.ToolStripMenuItem basicSettingsToolStripMenuItem; diff --git a/Source/LibationWinForms/Form1.Export.cs b/Source/LibationWinForms/Form1.Export.cs new file mode 100644 index 00000000..ae538cd2 --- /dev/null +++ b/Source/LibationWinForms/Form1.Export.cs @@ -0,0 +1,47 @@ +using System; +using System.Windows.Forms; +using ApplicationServices; + +namespace LibationWinForms +{ + public partial class Form1 + { + private void Configure_Export() { } + + private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e) + { + try + { + var saveFileDialog = new SaveFileDialog + { + Title = "Where to export Library", + Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*" + }; + + if (saveFileDialog.ShowDialog() != DialogResult.OK) + return; + + // FilterIndex is 1-based, NOT 0-based + switch (saveFileDialog.FilterIndex) + { + case 1: // xlsx + default: + LibraryExporter.ToXlsx(saveFileDialog.FileName); + break; + case 2: // csv + LibraryExporter.ToCsv(saveFileDialog.FileName); + break; + case 3: // json + LibraryExporter.ToJson(saveFileDialog.FileName); + break; + } + + MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName); + } + catch (Exception ex) + { + MessageBoxLib.ShowAdminAlert("Error attempting to export your library.", "Error exporting", ex); + } + } + } +} diff --git a/Source/LibationWinForms/Form1.Filter.cs b/Source/LibationWinForms/Form1.Filter.cs new file mode 100644 index 00000000..03e89f9d --- /dev/null +++ b/Source/LibationWinForms/Form1.Filter.cs @@ -0,0 +1,45 @@ +using System; +using System.Windows.Forms; +using LibationWinForms.Dialogs; + +namespace LibationWinForms +{ + public partial class Form1 + { + protected void Configure_Filter() { } + + private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog(); + + private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e) + { + if (e.KeyChar == (char)Keys.Return) + { + performFilter(this.filterSearchTb.Text); + + // silence the 'ding' + e.Handled = true; + } + } + + private void filterBtn_Click(object sender, EventArgs e) => performFilter(this.filterSearchTb.Text); + + private string lastGoodFilter = ""; + private void performFilter(string filterString) + { + this.filterSearchTb.Text = filterString; + + try + { + productsGrid.Filter(filterString); + lastGoodFilter = filterString; + } + catch (Exception ex) + { + MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error); + + // re-apply last good filter + performFilter(lastGoodFilter); + } + } + } +} diff --git a/Source/LibationWinForms/Form1.Liberate.cs b/Source/LibationWinForms/Form1.Liberate.cs new file mode 100644 index 00000000..43f91546 --- /dev/null +++ b/Source/LibationWinForms/Form1.Liberate.cs @@ -0,0 +1,30 @@ +using System; +using System.Windows.Forms; + +namespace LibationWinForms +{ + public partial class Form1 + { + private void Configure_Liberate() { } + + private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e) + => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(); + + private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e) + => await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(); + + private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e) + { + var result = MessageBox.Show( + "This converts all m4b titles in your library to mp3 files. Original files are not deleted." + + "\r\nFor large libraries this will take a long time and will take up more disk space." + + "\r\n\r\nContinue?" + + "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)", + "Convert all M4b => Mp3?", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning); + if (result == DialogResult.Yes) + await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync(); + } + } +} diff --git a/Source/LibationWinForms/Form1.PictureStorage.cs b/Source/LibationWinForms/Form1.PictureStorage.cs new file mode 100644 index 00000000..2a6efd09 --- /dev/null +++ b/Source/LibationWinForms/Form1.PictureStorage.cs @@ -0,0 +1,17 @@ +using Dinah.Core.Drawing; +using LibationFileManager; + +namespace LibationWinForms +{ + public partial class Form1 + { + private void Configure_PictureStorage() + { + // init default/placeholder cover art + var format = System.Drawing.Imaging.ImageFormat.Jpeg; + PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format)); + PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format)); + PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format)); + } + } +} diff --git a/Source/LibationWinForms/Form1.ProcessQueue.cs b/Source/LibationWinForms/Form1.ProcessQueue.cs new file mode 100644 index 00000000..2cdd7e72 --- /dev/null +++ b/Source/LibationWinForms/Form1.ProcessQueue.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using LibationFileManager; +using LibationWinForms.ProcessQueue; + +namespace LibationWinForms +{ + public partial class Form1 + { + private void Configure_ProcessQueue() + { + //splitContainer1.Panel2Collapsed = true; + processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut; + } + + private void ProcessBookQueue1_PopOut(object sender, EventArgs e) + { + ProcessBookForm dockForm = new(); + dockForm.WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth; + dockForm.RestoreSizeAndLocation(Configuration.Instance); + dockForm.FormClosing += DockForm_FormClosing; + splitContainer1.Panel2.Controls.Remove(processBookQueue1); + splitContainer1.Panel2Collapsed = true; + processBookQueue1.popoutBtn.Visible = false; + dockForm.PassControl(processBookQueue1); + dockForm.Show(); + this.Width -= dockForm.WidthChange; + } + + private void DockForm_FormClosing(object sender, FormClosingEventArgs e) + { + if (sender is ProcessBookForm dockForm) + { + this.Width += dockForm.WidthChange; + splitContainer1.Panel2.Controls.Add(dockForm.RegainControl()); + splitContainer1.Panel2Collapsed = false; + processBookQueue1.popoutBtn.Visible = true; + dockForm.SaveSizeAndLocation(Configuration.Instance); + this.Focus(); + } + } + } +} diff --git a/Source/LibationWinForms/Form1.QuickFilters.cs b/Source/LibationWinForms/Form1.QuickFilters.cs new file mode 100644 index 00000000..8804f083 --- /dev/null +++ b/Source/LibationWinForms/Form1.QuickFilters.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using LibationFileManager; +using LibationWinForms.Dialogs; + +namespace LibationWinForms +{ + public partial class Form1 + { + private void Configure_QuickFilters() + { + Load += updateFirstFilterIsDefaultToolStripMenuItem; + Load += updateFiltersMenu; + QuickFilters.UseDefaultChanged += updateFirstFilterIsDefaultToolStripMenuItem; + QuickFilters.Updated += updateFiltersMenu; + + productsGrid.InitialLoaded += (_, __) => + { + if (QuickFilters.UseDefault) + performFilter(QuickFilters.Filters.FirstOrDefault()); + }; + } + + private object quickFilterTag { get; } = new(); + private void updateFiltersMenu(object _ = null, object __ = null) + { + // remove old + var removeUs = quickFiltersToolStripMenuItem.DropDownItems + .Cast() + .Where(item => item.Tag == quickFilterTag) + .ToList(); + foreach (var item in removeUs) + quickFiltersToolStripMenuItem.DropDownItems.Remove(item); + + // re-populate + var index = 0; + foreach (var filter in QuickFilters.Filters) + { + var quickFilterMenuItem = new ToolStripMenuItem + { + Tag = quickFilterTag, + Text = $"&{++index}: {filter}" + }; + quickFilterMenuItem.Click += (_, __) => performFilter(filter); + quickFiltersToolStripMenuItem.DropDownItems.Add(quickFilterMenuItem); + } + } + + private void updateFirstFilterIsDefaultToolStripMenuItem(object sender, EventArgs e) + => firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault; + + private void firstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e) + => QuickFilters.UseDefault = !firstFilterIsDefaultToolStripMenuItem.Checked; + + private void addQuickFilterBtn_Click(object sender, EventArgs e) => QuickFilters.Add(this.filterSearchTb.Text); + + private void editQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters().ShowDialog(); + } +} diff --git a/Source/LibationWinForms/Form1.ScanAuto.cs b/Source/LibationWinForms/Form1.ScanAuto.cs new file mode 100644 index 00000000..30cfa7e1 --- /dev/null +++ b/Source/LibationWinForms/Form1.ScanAuto.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ApplicationServices; +using AudibleUtilities; +using Dinah.Core; +using LibationFileManager; + +namespace LibationWinForms +{ + // This is for the auto-scanner. It is unrelated to manual scanning/import + public partial class Form1 + { + private InterruptableTimer autoScanTimer; + + private void Configure_ScanAuto() + { + // creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok + var hours = 0; + var minutes = 5; + var seconds = 0; + var _5_minutes = new TimeSpan(hours, minutes, seconds); + autoScanTimer = new InterruptableTimer(_5_minutes); + + // subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI + autoScanTimer.Elapsed += async (_, __) => + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var accounts = persister.AccountsSettings + .GetAll() + .Where(a => a.LibraryScan) + .ToArray(); + + // in autoScan, new books SHALL NOT show dialog + await Invoke(async () => await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts)); + }; + + // load init state to menu checkbox + Load += updateAutoScanLibraryToolStripMenuItem; + // if enabled: begin on load + Load += startAutoScan; + + // if new 'default' account is added, run autoscan + AccountsSettingsPersister.Saving += accountsPreSave; + AccountsSettingsPersister.Saved += accountsPostSave; + + // when autoscan setting is changed, update menu checkbox and run autoscan + Configuration.Instance.AutoScanChanged += updateAutoScanLibraryToolStripMenuItem; + Configuration.Instance.AutoScanChanged += startAutoScan; + } + + private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts; + private List<(string AccountId, string LocaleName)> getDefaultAccounts() + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + return persister.AccountsSettings + .GetAll() + .Where(a => a.LibraryScan) + .Select(a => (a.AccountId, a.Locale.Name)) + .ToList(); + } + private void accountsPreSave(object sender = null, EventArgs e = null) + => preSaveDefaultAccounts = getDefaultAccounts(); + private void accountsPostSave(object sender = null, EventArgs e = null) + { + var postSaveDefaultAccounts = getDefaultAccounts(); + var newDefaultAccounts = postSaveDefaultAccounts.Except(preSaveDefaultAccounts).ToList(); + + if (newDefaultAccounts.Any()) + startAutoScan(); + } + + private void startAutoScan(object sender = null, EventArgs e = null) + { + if (Configuration.Instance.AutoScan) + autoScanTimer.PerformNow(); + else + autoScanTimer.Stop(); + } + + private void updateAutoScanLibraryToolStripMenuItem(object sender, EventArgs e) => autoScanLibraryToolStripMenuItem.Checked = Configuration.Instance.AutoScan; + + private void autoScanLibraryToolStripMenuItem_Click(object sender, EventArgs e) => Configuration.Instance.AutoScan = !autoScanLibraryToolStripMenuItem.Checked; + } +} diff --git a/Source/LibationWinForms/Form1.ScanManual.cs b/Source/LibationWinForms/Form1.ScanManual.cs new file mode 100644 index 00000000..1ba25b6f --- /dev/null +++ b/Source/LibationWinForms/Form1.ScanManual.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using ApplicationServices; +using AudibleUtilities; +using LibationFileManager; +using LibationWinForms.Dialogs; + +namespace LibationWinForms +{ + // this is for manual scan/import. Unrelated to auto-scan + public partial class Form1 + { + private void Configure_ScanManual() + { + this.Load += refreshImportMenu; + AccountsSettingsPersister.Saved += refreshImportMenu; + } + + private void refreshImportMenu(object _, EventArgs __) + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var count = persister.AccountsSettings.Accounts.Count; + + autoScanLibraryToolStripMenuItem.Visible = count > 0; + + noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0; + scanLibraryToolStripMenuItem.Visible = count == 1; + scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1; + scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1; + + removeLibraryBooksToolStripMenuItem.Visible = count > 0; + removeSomeAccountsToolStripMenuItem.Visible = count > 1; + removeAllAccountsToolStripMenuItem.Visible = count > 1; + } + + private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e) + { + MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account"); + new AccountsDialog().ShowDialog(); + } + + private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e) + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault(); + await scanLibrariesAsync(firstAccount); + } + + private async void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var allAccounts = persister.AccountsSettings.GetAll(); + await scanLibrariesAsync(allAccounts); + } + + private async void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var scanAccountsDialog = new ScanAccountsDialog(); + + if (scanAccountsDialog.ShowDialog() != DialogResult.OK) + return; + + if (!scanAccountsDialog.CheckedAccounts.Any()) + return; + + await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts); + } + + private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e) + { + // if 0 accounts, this will not be visible + // if 1 account, run scanLibrariesRemovedBooks() on this account + // if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks() + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var accounts = persister.AccountsSettings.GetAll(); + + if (accounts.Count != 1) + return; + + var firstAccount = accounts.Single(); + scanLibrariesRemovedBooks(firstAccount); + } + + // selectively remove books from all accounts + private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var allAccounts = persister.AccountsSettings.GetAll(); + scanLibrariesRemovedBooks(allAccounts.ToArray()); + } + + // selectively remove books from some accounts + private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var scanAccountsDialog = new ScanAccountsDialog(); + + if (scanAccountsDialog.ShowDialog() != DialogResult.OK) + return; + + if (!scanAccountsDialog.CheckedAccounts.Any()) + return; + + scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray()); + } + + private void scanLibrariesRemovedBooks(params Account[] accounts) + { + using var dialog = new RemoveBooksDialog(accounts); + dialog.ShowDialog(); + } + + private async Task scanLibrariesAsync(IEnumerable accounts) => await scanLibrariesAsync(accounts.ToArray()); + private async Task scanLibrariesAsync(params Account[] accounts) + { + try + { + var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts); + + // this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop + if (Configuration.Instance.ShowImportedStats && newAdded > 0) + MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}"); + } + catch (Exception ex) + { + MessageBoxLib.ShowAdminAlert( + "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator", + "Error importing library", + ex); + } + } + } +} diff --git a/Source/LibationWinForms/Form1.ScanNotification.cs b/Source/LibationWinForms/Form1.ScanNotification.cs new file mode 100644 index 00000000..d6c5fa10 --- /dev/null +++ b/Source/LibationWinForms/Form1.ScanNotification.cs @@ -0,0 +1,37 @@ +using System; +using ApplicationServices; + +namespace LibationWinForms +{ + // This is for the Scanning notificationin the upper right. This shown for manual scanning and auto-scan + public partial class Form1 + { + private void Configure_ScanNotification() + { + LibraryCommands.ScanBegin += LibraryCommands_ScanBegin; + LibraryCommands.ScanEnd += LibraryCommands_ScanEnd; + } + + private void LibraryCommands_ScanBegin(object sender, int accountsLength) + { + scanLibraryToolStripMenuItem.Enabled = false; + scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false; + scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false; + + this.scanningToolStripMenuItem.Visible = true; + this.scanningToolStripMenuItem.Text + = (accountsLength == 1) + ? "Scanning..." + : $"Scanning {accountsLength} accounts..."; + } + + private void LibraryCommands_ScanEnd(object sender, EventArgs e) + { + scanLibraryToolStripMenuItem.Enabled = true; + scanLibraryOfAllAccountsToolStripMenuItem.Enabled = true; + scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = true; + + this.scanningToolStripMenuItem.Visible = false; + } + } +} diff --git a/Source/LibationWinForms/Form1.Settings.cs b/Source/LibationWinForms/Form1.Settings.cs new file mode 100644 index 00000000..9bee9b4c --- /dev/null +++ b/Source/LibationWinForms/Form1.Settings.cs @@ -0,0 +1,18 @@ +using System; +using System.Windows.Forms; +using LibationWinForms.Dialogs; + +namespace LibationWinForms +{ + public partial class Form1 + { + private void Configure_Settings() { } + + private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog().ShowDialog(); + + private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog(); + + private void aboutToolStripMenuItem_Click(object sender, EventArgs e) + => MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}"); + } +} diff --git a/Source/LibationWinForms/Form1.VisibleBooks.cs b/Source/LibationWinForms/Form1.VisibleBooks.cs new file mode 100644 index 00000000..848f6c09 --- /dev/null +++ b/Source/LibationWinForms/Form1.VisibleBooks.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using ApplicationServices; +using Dinah.Core.Threading; +using LibationWinForms.Dialogs; + +namespace LibationWinForms +{ + public partial class Form1 + { + private string visibleBooksToolStripMenuItem_format; + private string liberateVisibleToolStripMenuItem_format; + private string liberateVisible2ToolStripMenuItem_format; + + protected void Configure_VisibleBooks() + { + // bottom-left visible count + productsGrid.VisibleCountChanged += (_, qty) => visibleCountLbl.Text = string.Format("Visible: {0}", qty); + + // back up string formats + visibleBooksToolStripMenuItem_format = visibleBooksToolStripMenuItem.Text; + liberateVisibleToolStripMenuItem_format = liberateVisibleToolStripMenuItem.Text; + liberateVisible2ToolStripMenuItem_format = liberateVisible2ToolStripMenuItem.Text; + + productsGrid.VisibleCountChanged += (_, qty) => { + visibleBooksToolStripMenuItem.Text = string.Format(visibleBooksToolStripMenuItem_format, qty); + visibleBooksToolStripMenuItem.Enabled = qty > 0; + + var notLiberatedCount = productsGrid.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated); + }; + + productsGrid.VisibleCountChanged += setLiberatedVisibleMenuItemAsync; + LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync; + } + private async void setLiberatedVisibleMenuItemAsync(object _, int __) + => await Task.Run(setLiberatedVisibleMenuItem); + private async void setLiberatedVisibleMenuItemAsync(object _, EventArgs __) + => await Task.Run(setLiberatedVisibleMenuItem); + void setLiberatedVisibleMenuItem() + { + var notLiberated = productsGrid.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated); + this.UIThreadSync(() => + { + if (notLiberated > 0) + { + liberateVisibleToolStripMenuItem.Text = string.Format(liberateVisibleToolStripMenuItem_format, notLiberated); + liberateVisibleToolStripMenuItem.Enabled = true; + + liberateVisible2ToolStripMenuItem.Text = string.Format(liberateVisible2ToolStripMenuItem_format, notLiberated); + liberateVisible2ToolStripMenuItem.Enabled = true; + } + else + { + liberateVisibleToolStripMenuItem.Text = "All visible books are liberated"; + liberateVisibleToolStripMenuItem.Enabled = false; + + liberateVisible2ToolStripMenuItem.Text = "All visible books are liberated"; + liberateVisible2ToolStripMenuItem.Enabled = false; + } + }); + } + + private async void liberateVisible(object sender, EventArgs e) + => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(productsGrid.GetVisible()); + + private void replaceTagsToolStripMenuItem_Click(object sender, EventArgs e) + { + var dialog = new TagsBatchDialog(); + var result = dialog.ShowDialog(); + if (result != DialogResult.OK) + return; + + var visibleLibraryBooks = productsGrid.GetVisible(); + + var confirmationResult = MessageBoxLib.ShowConfirmationDialog( + visibleLibraryBooks, + $"Are you sure you want to replace tags in {0}?", + "Replace tags?"); + + if (confirmationResult != DialogResult.Yes) + return; + + foreach (var libraryBook in visibleLibraryBooks) + libraryBook.Book.UserDefinedItem.Tags = dialog.NewTags; + LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book)); + } + + private void setDownloadedToolStripMenuItem_Click(object sender, EventArgs e) + { + var dialog = new LiberatedStatusBatchDialog(); + var result = dialog.ShowDialog(); + if (result != DialogResult.OK) + return; + + var visibleLibraryBooks = productsGrid.GetVisible(); + + var confirmationResult = MessageBoxLib.ShowConfirmationDialog( + visibleLibraryBooks, + $"Are you sure you want to replace downloaded status in {0}?", + "Replace downloaded status?"); + + if (confirmationResult != DialogResult.Yes) + return; + + foreach (var libraryBook in visibleLibraryBooks) + libraryBook.Book.UserDefinedItem.BookStatus = dialog.BookLiberatedStatus; + LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book)); + } + + private async void removeToolStripMenuItem_Click(object sender, EventArgs e) + { + var visibleLibraryBooks = productsGrid.GetVisible(); + + var confirmationResult = MessageBoxLib.ShowConfirmationDialog( + visibleLibraryBooks, + $"Are you sure you want to remove {0} from Libation's library?", + "Remove books from Libation?"); + + if (confirmationResult != DialogResult.Yes) + return; + + var visibleIds = visibleLibraryBooks.Select(lb => lb.Book.AudibleProductId).ToList(); + await LibraryCommands.RemoveBooksAsync(visibleIds); + } + } +} diff --git a/Source/LibationWinForms/Form1.cs b/Source/LibationWinForms/Form1.cs index 30a96998..e1128d25 100644 --- a/Source/LibationWinForms/Form1.cs +++ b/Source/LibationWinForms/Form1.cs @@ -4,25 +4,14 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; using ApplicationServices; -using AudibleUtilities; using Dinah.Core; -using Dinah.Core.Drawing; using Dinah.Core.Threading; using LibationFileManager; -using LibationWinForms.Dialogs; -using LibationWinForms.ProcessQueue; namespace LibationWinForms { public partial class Form1 : Form { - private string beginBookBackupsToolStripMenuItem_format { get; } - private string beginPdfBackupsToolStripMenuItem_format { get; } - - private string visibleBooksToolStripMenuItem_format { get; } - private string liberateVisibleToolStripMenuItem_format { get; } - private string liberateVisible2ToolStripMenuItem_format { get; } - private ProductsGrid productsGrid { get; } public Form1() @@ -32,55 +21,51 @@ namespace LibationWinForms if (this.DesignMode) return; - //splitContainer1.Panel2Collapsed = true; - processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut; - - productsGrid = new ProductsGrid { Dock = DockStyle.Fill }; - productsGrid.VisibleCountChanged += (_, qty) => visibleCountLbl.Text = string.Format("Visible: {0}", qty); - gridPanel.Controls.Add(productsGrid); - this.Load += (_, __) => { - productsGrid.Display(); + // I'd actually like these lines to be handled in the designer, but I'm currently getting this error when I try: + // Failed to create component 'ProductsGrid'. The error message follows: + // 'Microsoft.DotNet.DesignTools.Client.DesignToolsServerException: Object reference not set to an instance of an object. + // Since the designer's choking on it, I'm keeping it below the DesignMode check to be safe + productsGrid = new ProductsGrid { Dock = DockStyle.Fill }; + gridPanel.Controls.Add(productsGrid); + } - // also applies filter. ONLY call AFTER loading grid - loadInitialQuickFilterState(); - }; - - // back up string formats - beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text; - beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text; - visibleBooksToolStripMenuItem_format = visibleBooksToolStripMenuItem.Text; - liberateVisibleToolStripMenuItem_format = liberateVisibleToolStripMenuItem.Text; - liberateVisible2ToolStripMenuItem_format = liberateVisible2ToolStripMenuItem.Text; - - // independent UI updates this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance); this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance); - LibraryCommands.LibrarySizeChanged += (_, __) => + + // this looks like a perfect opportunity to refactor per below. + // since this loses design-time tooling and internal access, for now I'm opting for partial classes + // var modules = new ConfigurableModuleBase[] + // { + // new PictureStorageModule(), + // new BackupCountsModule(), + // new VisibleBooksModule(), + // // ... + // }; + // foreach(ConfigurableModuleBase m in modules) + // m.Configure(this); + + // these should do nothing interesting yet (storing simple var, subscribe to events) and should never rely on each other for order. + // otherwise, order could be an issue. + // eg: if one of these init'd productsGrid, then another can't reliably subscribe to it + Configure_PictureStorage(); + Configure_BackupCounts(); + Configure_ScanAuto(); + Configure_ScanNotification(); + Configure_VisibleBooks(); + Configure_QuickFilters(); + Configure_ScanManual(); + Configure_Liberate(); + Configure_Export(); + Configure_Settings(); + Configure_ProcessQueue(); + Configure_Filter(); + + // Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1' { - this.UIThreadSync(() => productsGrid.Display()); - this.UIThreadAsync(() => doFilter(lastGoodFilter)); - }; - LibraryCommands.LibrarySizeChanged += setBackupCounts; - this.Load += setBackupCounts; - LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts; - QuickFilters.Updated += updateFiltersMenu; - LibraryCommands.ScanBegin += LibraryCommands_ScanBegin; - LibraryCommands.ScanEnd += LibraryCommands_ScanEnd; - - // accounts updated - this.Load += refreshImportMenu; - AccountsSettingsPersister.Saved += refreshImportMenu; - - configAndInitAutoScan(); - - configVisibleBooksMenu(); - - // init default/placeholder cover art - var format = System.Drawing.Imaging.ImageFormat.Jpeg; - PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format)); - PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format)); - PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format)); + this.Load += (_, __) => productsGrid.Display(); + LibraryCommands.LibrarySizeChanged += (_, __) => this.UIThreadAsync(() => productsGrid.Display()); + } } private void Form1_Load(object sender, EventArgs e) @@ -90,596 +75,5 @@ namespace LibationWinForms // I'm leaving this empty call here as a reminder that if we use this, it should probably be after DesignMode check } - - #region bottom: backup counts - private System.ComponentModel.BackgroundWorker updateCountsBw; - private bool runBackupCountsAgain; - - private void setBackupCounts(object _, object __) - { - runBackupCountsAgain = true; - - if (updateCountsBw is not null) - return; - - updateCountsBw = new System.ComponentModel.BackgroundWorker(); - updateCountsBw.DoWork += UpdateCountsBw_DoWork; - updateCountsBw.RunWorkerCompleted += UpdateCountsBw_RunWorkerCompleted; - updateCountsBw.RunWorkerAsync(); - } - - private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e) - { - while (runBackupCountsAgain) - { - runBackupCountsAgain = false; - - var libraryStats = LibraryCommands.GetCounts(); - e.Result = libraryStats; - } - updateCountsBw = null; - } - - private void UpdateCountsBw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) - { - var libraryStats = e.Result as LibraryCommands.LibraryStats; - - setBookBackupCounts(libraryStats); - setPdfBackupCounts(libraryStats); - } - - private void setBookBackupCounts(LibraryCommands.LibraryStats libraryStats) - { - var backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}"; - - // enable/disable export - var hasResults = 0 < (libraryStats.booksFullyBackedUp + libraryStats.booksDownloadedOnly + libraryStats.booksNoProgress + libraryStats.booksError); - exportLibraryToolStripMenuItem.Enabled = hasResults; - - // update bottom numbers - var pending = libraryStats.booksNoProgress + libraryStats.booksDownloadedOnly; - var statusStripText - = !hasResults ? "No books. Begin by importing your library" - : libraryStats.booksError > 0 ? string.Format(backupsCountsLbl_Format + " Errors: {3}", libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp, libraryStats.booksError) - : pending > 0 ? string.Format(backupsCountsLbl_Format, libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp) - : $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up"; - - // update menu item - var menuItemText - = pending > 0 - ? $"{pending} remaining" - : "All books have been liberated"; - - // update UI - statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText); - menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0); - menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText)); - } - private void setPdfBackupCounts(LibraryCommands.LibraryStats libraryStats) - { - var pdfsCountsLbl_Format = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}"; - - // update bottom numbers - var hasResults = 0 < (libraryStats.pdfsNotDownloaded + libraryStats.pdfsDownloaded); - var statusStripText - = !hasResults ? "" - : libraryStats.pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded) - : $"| All {libraryStats.pdfsDownloaded} PDFs downloaded"; - - // update menu item - var menuItemText - = libraryStats.pdfsNotDownloaded > 0 - ? $"{libraryStats.pdfsNotDownloaded} remaining" - : "All PDFs have been downloaded"; - - // update UI - statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText); - menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0); - menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText)); - } - #endregion - - #region filter - private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog(); - - private void AddFilterBtn_Click(object sender, EventArgs e) => QuickFilters.Add(this.filterSearchTb.Text); - - private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e) - { - if (e.KeyChar == (char)Keys.Return) - { - doFilter(); - - // silence the 'ding' - e.Handled = true; - } - } - private void filterBtn_Click(object sender, EventArgs e) => doFilter(); - - private string lastGoodFilter = ""; - private void doFilter(string filterString) - { - this.filterSearchTb.Text = filterString; - doFilter(); - } - private void doFilter() - { - try - { - productsGrid.Filter(filterSearchTb.Text); - lastGoodFilter = filterSearchTb.Text; - } - catch (Exception ex) - { - MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error); - - // re-apply last good filter - doFilter(lastGoodFilter); - } - } - #endregion - - #region Auto-scanner - private InterruptableTimer autoScanTimer; - - private void configAndInitAutoScan() - { - var hours = 0; - var minutes = 5; - var seconds = 0; - var _5_minutes = new TimeSpan(hours, minutes, seconds); - autoScanTimer = new InterruptableTimer(_5_minutes); - - // subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI - autoScanTimer.Elapsed += async (_, __) => - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var accounts = persister.AccountsSettings - .GetAll() - .Where(a => a.LibraryScan) - .ToArray(); - - // in autoScan, new books SHALL NOT show dialog - await Invoke(async () => await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts)); - }; - - // load init state to menu checkbox - this.Load += updateAutoScanLibraryToolStripMenuItem; - // if enabled: begin on load - this.Load += startAutoScan; - - // if new 'default' account is added, run autoscan - AccountsSettingsPersister.Saving += accountsPreSave; - AccountsSettingsPersister.Saved += accountsPostSave; - - // when autoscan setting is changed, update menu checkbox and run autoscan - Configuration.Instance.AutoScanChanged += updateAutoScanLibraryToolStripMenuItem; - Configuration.Instance.AutoScanChanged += startAutoScan; - } - - private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts; - private List<(string AccountId, string LocaleName)> getDefaultAccounts() - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - return persister.AccountsSettings - .GetAll() - .Where(a => a.LibraryScan) - .Select(a => (a.AccountId, a.Locale.Name)) - .ToList(); - } - private void accountsPreSave(object sender = null, EventArgs e = null) - => preSaveDefaultAccounts = getDefaultAccounts(); - private void accountsPostSave(object sender = null, EventArgs e = null) - { - var postSaveDefaultAccounts = getDefaultAccounts(); - var newDefaultAccounts = postSaveDefaultAccounts.Except(preSaveDefaultAccounts).ToList(); - - if (newDefaultAccounts.Any()) - startAutoScan(); - } - - private void startAutoScan(object sender = null, EventArgs e = null) - { - if (Configuration.Instance.AutoScan) - autoScanTimer.PerformNow(); - else - autoScanTimer.Stop(); - } - - private void updateAutoScanLibraryToolStripMenuItem(object sender, EventArgs e) => autoScanLibraryToolStripMenuItem.Checked = Configuration.Instance.AutoScan; - #endregion - - #region Import menu - private void refreshImportMenu(object _ = null, EventArgs __ = null) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var count = persister.AccountsSettings.Accounts.Count; - - autoScanLibraryToolStripMenuItem.Visible = count > 0; - - noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0; - scanLibraryToolStripMenuItem.Visible = count == 1; - scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1; - scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1; - - removeLibraryBooksToolStripMenuItem.Visible = count > 0; - removeSomeAccountsToolStripMenuItem.Visible = count > 1; - removeAllAccountsToolStripMenuItem.Visible = count > 1; - } - - private void autoScanLibraryToolStripMenuItem_Click(object sender, EventArgs e) => Configuration.Instance.AutoScan = !autoScanLibraryToolStripMenuItem.Checked; - - private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e) - { - MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account"); - new AccountsDialog().ShowDialog(); - } - - private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault(); - await scanLibrariesAsync(firstAccount); - } - - private async void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var allAccounts = persister.AccountsSettings.GetAll(); - await scanLibrariesAsync(allAccounts); - } - - private async void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e) - { - using var scanAccountsDialog = new ScanAccountsDialog(); - - if (scanAccountsDialog.ShowDialog() != DialogResult.OK) - return; - - if (!scanAccountsDialog.CheckedAccounts.Any()) - return; - - await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts); - } - - private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e) - { - // if 0 accounts, this will not be visible - // if 1 account, run scanLibrariesRemovedBooks() on this account - // if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks() - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var accounts = persister.AccountsSettings.GetAll(); - - if (accounts.Count != 1) - return; - - var firstAccount = accounts.Single(); - scanLibrariesRemovedBooks(firstAccount); - } - - // selectively remove books from all accounts - private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var allAccounts = persister.AccountsSettings.GetAll(); - scanLibrariesRemovedBooks(allAccounts.ToArray()); - } - - // selectively remove books from some accounts - private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e) - { - using var scanAccountsDialog = new ScanAccountsDialog(); - - if (scanAccountsDialog.ShowDialog() != DialogResult.OK) - return; - - if (!scanAccountsDialog.CheckedAccounts.Any()) - return; - - scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray()); - } - - private void scanLibrariesRemovedBooks(params Account[] accounts) - { - using var dialog = new RemoveBooksDialog(accounts); - dialog.ShowDialog(); - } - - private async Task scanLibrariesAsync(IEnumerable accounts) => await scanLibrariesAsync(accounts.ToArray()); - private async Task scanLibrariesAsync(params Account[] accounts) - { - try - { - var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts); - - // this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop - if (Configuration.Instance.ShowImportedStats && newAdded > 0) - MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}"); - } - catch (Exception ex) - { - MessageBoxLib.ShowAdminAlert( - "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator", - "Error importing library", - ex); - } - } - #endregion - - #region Liberate menu - private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e) - => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(); - - private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e) - => await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(); - - private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e) - { - var result = MessageBox.Show( - "This converts all m4b titles in your library to mp3 files. Original files are not deleted." - + "\r\nFor large libraries this will take a long time and will take up more disk space." - + "\r\n\r\nContinue?" - + "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)", - "Convert all M4b => Mp3?", - MessageBoxButtons.YesNo, - MessageBoxIcon.Warning); - if (result == DialogResult.Yes) - await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync(); - } - - #endregion - - #region Export menu - private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e) - { - try - { - var saveFileDialog = new SaveFileDialog - { - Title = "Where to export Library", - Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*" - }; - - if (saveFileDialog.ShowDialog() != DialogResult.OK) - return; - - // FilterIndex is 1-based, NOT 0-based - switch (saveFileDialog.FilterIndex) - { - case 1: // xlsx - default: - LibraryExporter.ToXlsx(saveFileDialog.FileName); - break; - case 2: // csv - LibraryExporter.ToCsv(saveFileDialog.FileName); - break; - case 3: // json - LibraryExporter.ToJson(saveFileDialog.FileName); - break; - } - - MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName); - } - catch (Exception ex) - { - MessageBoxLib.ShowAdminAlert("Error attempting to export your library.", "Error exporting", ex); - } - } - #endregion - - #region Quick Filters menu - private void FirstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e) - { - firstFilterIsDefaultToolStripMenuItem.Checked = !firstFilterIsDefaultToolStripMenuItem.Checked; - QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem.Checked; - } - - private void loadInitialQuickFilterState() - { - // set inital state. do once only - firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault; - - // load default filter. do once only - if (QuickFilters.UseDefault) - doFilter(QuickFilters.Filters.FirstOrDefault()); - - updateFiltersMenu(); - } - - private object quickFilterTag { get; } = new object(); - private void updateFiltersMenu(object _ = null, object __ = null) - { - // remove old - for (var i = quickFiltersToolStripMenuItem.DropDownItems.Count - 1; i >= 0; i--) - { - var menuItem = quickFiltersToolStripMenuItem.DropDownItems[i]; - if (menuItem.Tag == quickFilterTag) - quickFiltersToolStripMenuItem.DropDownItems.Remove(menuItem); - } - - // re-populate - var index = 0; - foreach (var filter in QuickFilters.Filters) - { - var menuItem = new ToolStripMenuItem - { - Tag = quickFilterTag, - Text = $"&{++index}: {filter}" - }; - menuItem.Click += (_, __) => doFilter(filter); - quickFiltersToolStripMenuItem.DropDownItems.Add(menuItem); - } - } - - private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters().ShowDialog(); - #endregion - - #region Visible Books menu - private void configVisibleBooksMenu() - { - productsGrid.VisibleCountChanged += (_, qty) => { - visibleBooksToolStripMenuItem.Text = string.Format(visibleBooksToolStripMenuItem_format, qty); - visibleBooksToolStripMenuItem.Enabled = qty > 0; - - var notLiberatedCount = productsGrid.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated); - }; - - productsGrid.VisibleCountChanged += setLiberatedVisibleMenuItemAsync; - LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync; - } - private async void setLiberatedVisibleMenuItemAsync(object _, int __) - => await Task.Run(setLiberatedVisibleMenuItem); - private async void setLiberatedVisibleMenuItemAsync(object _, EventArgs __) - => await Task.Run(setLiberatedVisibleMenuItem); - void setLiberatedVisibleMenuItem() - { - var notLiberated = productsGrid.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated); - this.UIThreadSync(() => - { - if (notLiberated > 0) - { - liberateVisibleToolStripMenuItem.Text = string.Format(liberateVisibleToolStripMenuItem_format, notLiberated); - liberateVisibleToolStripMenuItem.Enabled = true; - - liberateVisible2ToolStripMenuItem.Text = string.Format(liberateVisible2ToolStripMenuItem_format, notLiberated); - liberateVisible2ToolStripMenuItem.Enabled = true; - } - else - { - liberateVisibleToolStripMenuItem.Text = "All visible books are liberated"; - liberateVisibleToolStripMenuItem.Enabled = false; - - liberateVisible2ToolStripMenuItem.Text = "All visible books are liberated"; - liberateVisible2ToolStripMenuItem.Enabled = false; - } - }); - } - - private async void liberateVisible(object sender, EventArgs e) - => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(productsGrid.GetVisible()); - - private void replaceTagsToolStripMenuItem_Click(object sender, EventArgs e) - { - var dialog = new TagsBatchDialog(); - var result = dialog.ShowDialog(); - if (result != DialogResult.OK) - return; - - var visibleLibraryBooks = productsGrid.GetVisible(); - - var confirmationResult = MessageBoxLib.ShowConfirmationDialog( - visibleLibraryBooks, - $"Are you sure you want to replace tags in {0}?", - "Replace tags?"); - - if (confirmationResult != DialogResult.Yes) - return; - - foreach (var libraryBook in visibleLibraryBooks) - libraryBook.Book.UserDefinedItem.Tags = dialog.NewTags; - LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book)); - } - - private void setDownloadedToolStripMenuItem_Click(object sender, EventArgs e) - { - var dialog = new LiberatedStatusBatchDialog(); - var result = dialog.ShowDialog(); - if (result != DialogResult.OK) - return; - - var visibleLibraryBooks = productsGrid.GetVisible(); - - var confirmationResult = MessageBoxLib.ShowConfirmationDialog( - visibleLibraryBooks, - $"Are you sure you want to replace downloaded status in {0}?", - "Replace downloaded status?"); - - if (confirmationResult != DialogResult.Yes) - return; - - foreach (var libraryBook in visibleLibraryBooks) - libraryBook.Book.UserDefinedItem.BookStatus = dialog.BookLiberatedStatus; - LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book)); - } - - private async void removeToolStripMenuItem_Click(object sender, EventArgs e) - { - var visibleLibraryBooks = productsGrid.GetVisible(); - - var confirmationResult = MessageBoxLib.ShowConfirmationDialog( - visibleLibraryBooks, - $"Are you sure you want to remove {0} from Libation's library?", - "Remove books from Libation?"); - - if (confirmationResult != DialogResult.Yes) - return; - - var visibleIds = visibleLibraryBooks.Select(lb => lb.Book.AudibleProductId).ToList(); - await LibraryCommands.RemoveBooksAsync(visibleIds); - } - #endregion - - #region Settings menu - private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog().ShowDialog(); - - private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog(); - - private void aboutToolStripMenuItem_Click(object sender, EventArgs e) - => MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}"); - #endregion - - #region Scanning label - private void LibraryCommands_ScanBegin(object sender, int accountsLength) - { - scanLibraryToolStripMenuItem.Enabled = false; - scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false; - scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false; - - this.scanningToolStripMenuItem.Visible = true; - this.scanningToolStripMenuItem.Text - = (accountsLength == 1) - ? "Scanning..." - : $"Scanning {accountsLength} accounts..."; - } - - private void LibraryCommands_ScanEnd(object sender, EventArgs e) - { - scanLibraryToolStripMenuItem.Enabled = true; - scanLibraryOfAllAccountsToolStripMenuItem.Enabled = true; - scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = true; - - this.scanningToolStripMenuItem.Visible = false; - } - #endregion - - #region Process Queue - - private void ProcessBookQueue1_PopOut(object sender, EventArgs e) - { - ProcessBookForm dockForm = new(); - dockForm.WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth; - dockForm.RestoreSizeAndLocation(Configuration.Instance); - dockForm.FormClosing += DockForm_FormClosing; - splitContainer1.Panel2.Controls.Remove(processBookQueue1); - splitContainer1.Panel2Collapsed = true; - processBookQueue1.popoutBtn.Visible = false; - dockForm.PassControl(processBookQueue1); - dockForm.Show(); - this.Width -= dockForm.WidthChange; - } - - private void DockForm_FormClosing(object sender, FormClosingEventArgs e) - { - if (sender is ProcessBookForm dockForm) - { - this.Width += dockForm.WidthChange; - splitContainer1.Panel2.Controls.Add(dockForm.RegainControl()); - splitContainer1.Panel2Collapsed = false; - processBookQueue1.popoutBtn.Visible = true; - dockForm.SaveSizeAndLocation(Configuration.Instance); - this.Focus(); - } - } - #endregion } } diff --git a/Source/LibationWinForms/LibationWinForms.csproj b/Source/LibationWinForms/LibationWinForms.csproj index 4390a2b7..ee70ccaa 100644 --- a/Source/LibationWinForms/LibationWinForms.csproj +++ b/Source/LibationWinForms/LibationWinForms.csproj @@ -37,6 +37,12 @@ + + + Form1.cs + + + True diff --git a/Source/LibationWinForms/grid/ProductsGrid.cs b/Source/LibationWinForms/grid/ProductsGrid.cs index 76fe8f42..93caca4d 100644 --- a/Source/LibationWinForms/grid/ProductsGrid.cs +++ b/Source/LibationWinForms/grid/ProductsGrid.cs @@ -48,13 +48,16 @@ namespace LibationWinForms { InitializeComponent(); + if (this.DesignMode) + return; + + EnableDoubleBuffering(); + // sorting breaks filters. must reapply filters after sorting - _dataGridView.Sorted += Filter; + _dataGridView.Sorted += reapplyFilter; _dataGridView.CellContentClick += DataGridView_CellContentClick; this.Load += ProductsGrid_Load; - - EnableDoubleBuffering(); } private void EnableDoubleBuffering() @@ -158,6 +161,8 @@ namespace LibationWinForms private SortableBindingList bindingList; + private bool hasBeenDisplayed; + public event EventHandler InitialLoaded; public void Display() { // don't return early if lib size == 0. this will not update correctly if all books are removed @@ -172,14 +177,20 @@ namespace LibationWinForms // .ThenBy(lb => lb.Book.TitleSortable) .ToList(); - // BIND + // bind if (bindingList?.Count > 0) updateGrid(orderedBooks); else bindToGrid(orderedBooks); - // FILTER - Filter(); + // re-apply previous filter + reapplyFilter(); + + if (!hasBeenDisplayed) + { + hasBeenDisplayed = true; + InitialLoaded?.Invoke(this, new()); + } } private void bindToGrid(List orderedBooks) @@ -218,7 +229,7 @@ namespace LibationWinForms private GridEntry toGridEntry(DataLayer.LibraryBook libraryBook) { var entry = new GridEntry(libraryBook); - entry.Committed += Filter; + entry.Committed += reapplyFilter; entry.LibraryBookUpdated += (sender, _) => _dataGridView.InvalidateRow(_dataGridView.GetRowIdOfBoundItem((GridEntry)sender)); return entry; } @@ -228,9 +239,13 @@ namespace LibationWinForms #region Filter private string _filterSearchString; - private void Filter(object _ = null, EventArgs __ = null) => Filter(_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; + _filterSearchString = searchString; if (_dataGridView.Rows.Count == 0) @@ -279,6 +294,9 @@ namespace LibationWinForms // to ensure this is only ever called once: Load instead of 'override OnVisibleChanged' private void ProductsGrid_Load(object sender, EventArgs e) { + if (this.DesignMode) + return; + contextMenuStrip1.Items.Add(new ToolStripLabel("Show / Hide Columns")); contextMenuStrip1.Items.Add(new ToolStripSeparator());