From 4d1b78f7e4a434c7a03c2ad6f3ef9068d363b335 Mon Sep 17 00:00:00 2001 From: rmcrackan Date: Fri, 12 Jun 2026 09:53:57 -0400 Subject: [PATCH] #1865 - Add in-dialog "Apply to all" and "Remember in Settings" for bad-book errors. Also a testing section when run in debug (ie: from visual studio) --- .../FileLiberator/SimulateBadBookFailure.cs | 30 ++++ Source/LibationAvalonia/App.axaml.cs | 3 + .../Dialogs/BadBookActionDialog.axaml | 40 +++++ .../Dialogs/BadBookActionDialog.axaml.cs | 94 +++++++++++ .../ViewModels/MainVM.Debug.cs | 39 +++++ .../Views/MainWindow.axaml.cs | 33 ++++ .../LibationUiBase/BadBookActionDialogBase.cs | 35 ++++ .../ProcessQueue/BadBookSessionContext.cs | 10 ++ .../ProcessQueue/ProcessBookViewModel.cs | 45 ++++- .../ProcessQueue/ProcessQueueViewModel.cs | 36 +++- .../Dialogs/BadBookActionDialog.Designer.cs | 154 ++++++++++++++++++ .../Dialogs/BadBookActionDialog.cs | 68 ++++++++ Source/LibationWinForms/Form1.Debug.cs | 55 +++++++ Source/LibationWinForms/Form1.cs | 3 + Source/LibationWinForms/Program.cs | 17 ++ 15 files changed, 651 insertions(+), 11 deletions(-) create mode 100644 Source/FileLiberator/SimulateBadBookFailure.cs create mode 100644 Source/LibationAvalonia/Dialogs/BadBookActionDialog.axaml create mode 100644 Source/LibationAvalonia/Dialogs/BadBookActionDialog.axaml.cs create mode 100644 Source/LibationAvalonia/ViewModels/MainVM.Debug.cs create mode 100644 Source/LibationUiBase/BadBookActionDialogBase.cs create mode 100644 Source/LibationUiBase/ProcessQueue/BadBookSessionContext.cs create mode 100644 Source/LibationWinForms/Dialogs/BadBookActionDialog.Designer.cs create mode 100644 Source/LibationWinForms/Dialogs/BadBookActionDialog.cs create mode 100644 Source/LibationWinForms/Form1.Debug.cs diff --git a/Source/FileLiberator/SimulateBadBookFailure.cs b/Source/FileLiberator/SimulateBadBookFailure.cs new file mode 100644 index 00000000..56ea91eb --- /dev/null +++ b/Source/FileLiberator/SimulateBadBookFailure.cs @@ -0,0 +1,30 @@ +using DataLayer; +using Dinah.Core.ErrorHandling; +using LibationFileManager; +using System.Threading.Tasks; + +namespace FileLiberator; + +/// +/// Instantly fails processing so the bad-book error dialog can be tested without a real download failure. +/// +public class SimulateBadBookFailure : Processable, IProcessable +{ + public override string Name => "Simulate Bad Book Failure"; + + public override bool Validate(LibraryBook libraryBook) => true; + + public override Task ProcessAsync(LibraryBook libraryBook) + { + OnBegin(libraryBook); + + var result = new StatusHandler(); + result.AddError("Simulated processing failure for testing the bad-book error dialog."); + + OnCompleted(libraryBook); + return Task.FromResult(result); + } + + public static SimulateBadBookFailure Create(Configuration config) + => new() { Configuration = config }; +} diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs index 260e8ed8..bb3f5e13 100644 --- a/Source/LibationAvalonia/App.axaml.cs +++ b/Source/LibationAvalonia/App.axaml.cs @@ -47,6 +47,9 @@ public class App : Application MessageBoxBase.ShowAsyncImpl = (owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition) => MessageBox.Show(owner as Window, message, caption, buttons, icon, defaultButton, saveAndRestorePosition); + BadBookActionDialogBase.ShowAsyncImpl = (owner, message, caption) => + Dialogs.BadBookActionDialog.ShowAsync(owner as Window, message, caption); + if (LibraryTask is null) { RunSetupIfNeededAsync(desktop, Configuration.Instance); diff --git a/Source/LibationAvalonia/Dialogs/BadBookActionDialog.axaml b/Source/LibationAvalonia/Dialogs/BadBookActionDialog.axaml new file mode 100644 index 00000000..29a67b68 --- /dev/null +++ b/Source/LibationAvalonia/Dialogs/BadBookActionDialog.axaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Source/LibationAvalonia/Dialogs/BadBookActionDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/BadBookActionDialog.axaml.cs new file mode 100644 index 00000000..3604c73d --- /dev/null +++ b/Source/LibationAvalonia/Dialogs/BadBookActionDialog.axaml.cs @@ -0,0 +1,94 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; +using LibationUiBase; +using LibationUiBase.Forms; +using System.Threading.Tasks; + +namespace LibationAvalonia.Dialogs; + +public partial class BadBookActionDialog : DialogWindow +{ + public BadBookDialogResult Result { get; private set; } = new(DialogResult.Retry, false, false); + + public BadBookActionDialog() + { + InitializeComponent(); + SaveOnEnter = false; + CancelOnEscape = false; + } + + public BadBookActionDialog(string message, string caption) : this() + { + Title = caption; + messageTextBlock.Text = message; + ControlToFocusOnShow = retryButton; + } + + private void CloseWith(DialogResult action) + { + Result = new BadBookDialogResult( + action, + applyToAllCheckBox.IsChecked == true, + rememberInSettingsCheckBox.IsChecked == true); + Close(DialogResult.None); + } + + public void AbortButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) + => CloseWith(DialogResult.Abort); + + public void RetryButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) + => CloseWith(DialogResult.Retry); + + public void IgnoreButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) + => CloseWith(DialogResult.Ignore); + + public static Task ShowAsync(Window? owner, string message, string caption) + => Dispatcher.UIThread.InvokeAsync(async () => + { + owner = owner?.IsLoaded is true ? owner : null; + var dialog = new BadBookActionDialog(message, caption); + await DisplayDialogAsync(dialog, owner); + return dialog.Result; + }); + + private static async Task DisplayDialogAsync(BadBookActionDialog dialog, Window? owner) + { + if (owner is null) + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow?.IsLoaded is true) + await dialog.ShowDialog(desktop.MainWindow); + else + { + var tcs = new TaskCompletionSource(); + desktop.MainWindow = dialog; + dialog.Closed += (_, _) => tcs.SetResult(); + dialog.Show(); + await tcs.Task; + } + } + else + { + var window = new Window + { + IsVisible = false, + Height = 1, + Width = 1, + WindowDecorations = WindowDecorations.None, + ShowInTaskbar = false + }; + + window.Show(); + await dialog.ShowDialog(window); + window.Close(); + } + } + else + { + await dialog.ShowDialog(owner); + } + } +} diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Debug.cs b/Source/LibationAvalonia/ViewModels/MainVM.Debug.cs new file mode 100644 index 00000000..2c1f3650 --- /dev/null +++ b/Source/LibationAvalonia/ViewModels/MainVM.Debug.cs @@ -0,0 +1,39 @@ +#if DEBUG +using LibationUiBase.Forms; +using System.Linq; +using System.Threading.Tasks; + +namespace LibationAvalonia.ViewModels; + +partial class MainVM +{ + public async Task SimulateBadBookFailuresAsync() + { + var books = ProductsDisplay.GetVisibleBookEntries().Take(5).ToArray(); + if (books.Length == 0) + { + await MessageBox.Show( + "No books are visible in the grid.\n\nClear your filter or widen it, then try again.", + "Test bad book dialog", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + return; + } + + var confirm = await MessageBox.Show( + $"Queue {books.Length} visible book(s) with simulated failures?\n\n" + + "No files will be downloaded. Each book will immediately show the bad-book error dialog.\n\n" + + "Set error handling to \"Ask each time\" in Settings > Download/Decrypt before testing.", + "Test bad book dialog", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question, + MessageBoxDefaultButton.Button1); + + if (confirm != DialogResult.Yes) + return; + + ProcessQueue.QueueSimulatedBadBookFailures(books); + setQueueCollapseState(false); + } +} +#endif diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs index 50332f6c..3c83321b 100644 --- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs +++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs @@ -46,8 +46,41 @@ public partial class MainWindow : ReactiveWindow Configuration.Instance.PropertyChanged += Settings_PropertyChanged; Settings_PropertyChanged(this, null); DataContext = new MainVM(this); +#if DEBUG + Configure_DebugMenu(); +#endif } +#if DEBUG + private void Configure_DebugMenu() + { + var simulateItem = new MenuItem { Header = "Simulate bad book failures (test dialog)..." }; + simulateItem.Click += async (_, _) => + { + if (ViewModel is MainVM vm) + await vm.SimulateBadBookFailuresAsync(); + }; + + // Insert before Tour; the Separator above Tour in axaml already provides the divider. + var items = settingsToolStripMenuItem.Items; + var insertIndex = -1; + for (var i = 0; i < items.Count; i++) + { + if (items[i] is MenuItem menuItem + && menuItem.Header?.ToString()?.Contains("Tour", StringComparison.OrdinalIgnoreCase) == true) + { + insertIndex = i; + break; + } + } + + if (insertIndex < 0) + insertIndex = items.Count; + + items.Insert(insertIndex, simulateItem); + } +#endif + [Dinah.Core.PropertyChangeFilter(nameof(Configuration.Books))] private void Settings_PropertyChanged(object? sender, Dinah.Core.PropertyChangedEventArgsEx? e) { diff --git a/Source/LibationUiBase/BadBookActionDialogBase.cs b/Source/LibationUiBase/BadBookActionDialogBase.cs new file mode 100644 index 00000000..31a38f10 --- /dev/null +++ b/Source/LibationUiBase/BadBookActionDialogBase.cs @@ -0,0 +1,35 @@ +using LibationUiBase.Forms; +using System.Threading.Tasks; + +namespace LibationUiBase; + +public record BadBookDialogResult( + DialogResult Action, + bool ApplyToAll, + bool RememberInSettings); + +public delegate Task ShowBadBookDialogAsyncDelegate( + object? owner, string message, string caption); + +public static class BadBookActionDialogBase +{ + private static ShowBadBookDialogAsyncDelegate? s_ShowAsyncImpl; + + public static ShowBadBookDialogAsyncDelegate ShowAsyncImpl + { + get => s_ShowAsyncImpl ?? DefaultShowAsyncImpl; + set => s_ShowAsyncImpl = value; + } + + private static Task DefaultShowAsyncImpl(object? owner, string message, string caption) + { + Serilog.Log.Logger.Error("BadBookActionDialogBase implementation not set. {@DebugInfo}", new { owner, message, caption }); + return Task.FromResult(new BadBookDialogResult(DialogResult.Retry, false, false)); + } + + public static Task Show(string message, string caption) + => ShowAsyncImpl(null, message, caption); + + public static Task Show(object? owner, string message, string caption) + => ShowAsyncImpl(owner, message, caption); +} diff --git a/Source/LibationUiBase/ProcessQueue/BadBookSessionContext.cs b/Source/LibationUiBase/ProcessQueue/BadBookSessionContext.cs new file mode 100644 index 00000000..61fc49ea --- /dev/null +++ b/Source/LibationUiBase/ProcessQueue/BadBookSessionContext.cs @@ -0,0 +1,10 @@ +using LibationFileManager; + +namespace LibationUiBase.ProcessQueue; + +public class BadBookSessionContext +{ + public Configuration.BadBookAction? Override { get; set; } + + public void Reset() => Override = null; +} diff --git a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs index 069ac389..6516a1cc 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs @@ -45,6 +45,7 @@ public class ProcessBookViewModel : ReactiveObject { public LibraryBook LibraryBook { get; protected set; } public Configuration Configuration { get; } + private readonly BadBookSessionContext? _badBookSession; #region Properties exposed to the view public ProcessBookResult Result { get => field; set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(StatusText)); } } @@ -99,10 +100,11 @@ public class ProcessBookViewModel : ReactiveObject /// A series of Processable actions to perform on this book protected Queue> Processes { get; } = new(); - public ProcessBookViewModel(LibraryBook libraryBook, Configuration configuration) + public ProcessBookViewModel(LibraryBook libraryBook, Configuration configuration, BadBookSessionContext? badBookSession = null) { LibraryBook = libraryBook; Configuration = configuration; + _badBookSession = badBookSession; Title = LibraryBook.Book.TitleWithSubtitle; Author = LibraryBook.Book.AuthorNames; @@ -258,6 +260,7 @@ public class ProcessBookViewModel : ReactiveObject public ProcessBookViewModel AddDownloadPdf() => AddProcessable(); public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable(); public ProcessBookViewModel AddConvertToMp3() => AddProcessable(); + public ProcessBookViewModel AddSimulateBadBookFailure() => AddProcessable(); private ProcessBookViewModel AddProcessable() where T : Processable, IProcessable { @@ -374,12 +377,14 @@ public class ProcessBookViewModel : ReactiveObject const DialogResult SkipResult = DialogResult.Ignore; LogError($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}"); - DialogResult? dialogResult = Configuration.BadBook switch + DialogResult dialogResult = Configuration.BadBook switch { Configuration.BadBookAction.Abort => DialogResult.Abort, Configuration.BadBookAction.Retry => DialogResult.Retry, Configuration.BadBookAction.Ignore => DialogResult.Ignore, - Configuration.BadBookAction.Ask or _ => await ShowRetryDialogAsync(libraryBook) + Configuration.BadBookAction.Ask or _ => _badBookSession?.Override is Configuration.BadBookAction sessionOverride + ? ToDialogResult(sessionOverride) + : await ShowRetryDialogAsync(libraryBook) }; if (dialogResult == SkipResult) @@ -425,15 +430,21 @@ public class ProcessBookViewModel : ReactiveObject - IGNORE: Permanently ignore this book. Continue processing the queued books. (Will not try this book again later.) - See Settings in the Download/Decrypt tab to avoid this box in the future. + Check "Apply to all remaining books" to use your choice for the rest of this queue. + Check "Remember in Settings" to save your choice in Download/Decrypt settings. """; - const MessageBoxButtons SkipDialogButtons = MessageBoxButtons.AbortRetryIgnore; - const MessageBoxDefaultButton SkipDialogDefaultButton = MessageBoxDefaultButton.Button1; - try { - return await MessageBoxBase.Show(skipDialogText, "Skip this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton); + var result = await BadBookActionDialogBase.Show(skipDialogText, "Skip this book?"); + + if (result.ApplyToAll) + _badBookSession?.Override = ToBadBookAction(result.Action); + + if (result.RememberInSettings) + Configuration.BadBook = ToBadBookAction(result.Action); + + return result.Action; } catch (Exception ex) { @@ -442,5 +453,23 @@ public class ProcessBookViewModel : ReactiveObject } } + private static DialogResult ToDialogResult(Configuration.BadBookAction action) + => action switch + { + Configuration.BadBookAction.Abort => DialogResult.Abort, + Configuration.BadBookAction.Retry => DialogResult.Retry, + Configuration.BadBookAction.Ignore => DialogResult.Ignore, + _ => DialogResult.Retry + }; + + private static Configuration.BadBookAction ToBadBookAction(DialogResult action) + => action switch + { + DialogResult.Abort => Configuration.BadBookAction.Abort, + DialogResult.Retry => Configuration.BadBookAction.Retry, + DialogResult.Ignore => Configuration.BadBookAction.Ignore, + _ => Configuration.BadBookAction.Retry + }; + #endregion } \ No newline at end of file diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index 9fc2ac83..ac894b8c 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -21,6 +21,7 @@ public class ProcessQueueViewModel : ReactiveObject { public ObservableCollection LogEntries { get; } = new(); public TrackedQueue Queue { get; } = new(); + private readonly BadBookSessionContext _badBookSession = new(); public Task? QueueRunner { get; private set; } public bool Running => !QueueRunner?.IsCompleted ?? false; @@ -132,6 +133,34 @@ public class ProcessQueueViewModel : ReactiveObject return false; } + /// + /// Queues visible books with an instant simulated failure for testing the bad-book error dialog. + /// Does not download or modify files. + /// + public void QueueSimulatedBadBookFailures(IList libraryBooks, Configuration? config = null, int maxBooks = 5) + { + config ??= Configuration.Instance; + if (libraryBooks.Count == 0) + return; + + RunOnQueueUiThread(() => addSimulatedBadBookFailuresCore(libraryBooks, config, maxBooks)); + } + + private void addSimulatedBadBookFailuresCore(IList libraryBooks, Configuration config, int maxBooks) + { + var procs = libraryBooks + .Where(e => !IsBookInQueue(e)) + .Take(maxBooks) + .Select(entry => new ProcessBookViewModel(entry, config, _badBookSession).AddSimulateBadBookFailure()) + .ToArray(); + + if (procs.Length == 0) + return; + + Serilog.Log.Logger.Information("Queueing {count} books for simulated bad-book failure testing", procs.Length); + AddToQueue(procs); + } + public async Task QueueDownloadDecryptAsync(IList libraryBooks, Configuration? config = null) { config ??= Configuration.Instance; @@ -271,7 +300,7 @@ public class ProcessQueueViewModel : ReactiveObject AddToQueue(procs); ProcessBookViewModel Create(LibraryBook entry) - => new ProcessBookViewModel(entry, config).AddDownloadPdf(); + => new ProcessBookViewModel(entry, config, _badBookSession).AddDownloadPdf(); } private void AddDownloadDecrypt(IList entries, Configuration config) @@ -284,7 +313,7 @@ public class ProcessQueueViewModel : ReactiveObject AddToQueue(procs); ProcessBookViewModel Create(LibraryBook entry) - => new ProcessBookViewModel(entry, config).AddDownloadDecryptBook().AddDownloadPdf(); + => new ProcessBookViewModel(entry, config, _badBookSession).AddDownloadDecryptBook().AddDownloadPdf(); } private void AddConvertMp3(IList entries, Configuration config) @@ -297,7 +326,7 @@ public class ProcessQueueViewModel : ReactiveObject AddToQueue(procs); ProcessBookViewModel Create(LibraryBook entry) - => new ProcessBookViewModel(entry, config).AddConvertToMp3(); + => new ProcessBookViewModel(entry, config, _badBookSession).AddConvertToMp3(); } private void AddToQueue(IList pbook) @@ -319,6 +348,7 @@ public class ProcessQueueViewModel : ReactiveObject { Serilog.Log.Logger.Information("Begin processing queue"); + _badBookSession.Reset(); RunningTime = string.Empty; ProgressBarVisible = true; var startingTime = DateTime.Now; diff --git a/Source/LibationWinForms/Dialogs/BadBookActionDialog.Designer.cs b/Source/LibationWinForms/Dialogs/BadBookActionDialog.Designer.cs new file mode 100644 index 00000000..14deafee --- /dev/null +++ b/Source/LibationWinForms/Dialogs/BadBookActionDialog.Designer.cs @@ -0,0 +1,154 @@ +namespace LibationWinForms.Dialogs +{ + partial class BadBookActionDialog + { + private System.ComponentModel.IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + components.Dispose(); + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + this.pictureBox = new System.Windows.Forms.PictureBox(); + this.messageLbl = new System.Windows.Forms.Label(); + this.applyToAllCb = new System.Windows.Forms.CheckBox(); + this.rememberInSettingsCb = new System.Windows.Forms.CheckBox(); + this.abortBtn = new System.Windows.Forms.Button(); + this.retryBtn = new System.Windows.Forms.Button(); + this.ignoreBtn = new System.Windows.Forms.Button(); + this.buttonPanel = new System.Windows.Forms.Panel(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox)).BeginInit(); + this.buttonPanel.SuspendLayout(); + this.SuspendLayout(); + // + // pictureBox + // + this.pictureBox.Location = new System.Drawing.Point(12, 12); + this.pictureBox.Name = "pictureBox"; + this.pictureBox.Size = new System.Drawing.Size(32, 32); + this.pictureBox.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; + this.pictureBox.TabIndex = 0; + this.pictureBox.TabStop = false; + this.pictureBox.Image = System.Drawing.SystemIcons.Question.ToBitmap(); + // + // messageLbl + // + this.messageLbl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.messageLbl.AutoSize = true; + this.messageLbl.Location = new System.Drawing.Point(50, 12); + this.messageLbl.Name = "messageLbl"; + this.messageLbl.Size = new System.Drawing.Size(422, 15); + this.messageLbl.TabIndex = 1; + // + // applyToAllCb + // + this.applyToAllCb.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + this.applyToAllCb.AutoSize = true; + this.applyToAllCb.Location = new System.Drawing.Point(12, 185); + this.applyToAllCb.Name = "applyToAllCb"; + this.applyToAllCb.Size = new System.Drawing.Size(220, 19); + this.applyToAllCb.TabIndex = 2; + this.applyToAllCb.Text = "Apply to all remaining books in this queue"; + this.applyToAllCb.UseVisualStyleBackColor = true; + // + // rememberInSettingsCb + // + this.rememberInSettingsCb.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + this.rememberInSettingsCb.AutoSize = true; + this.rememberInSettingsCb.Location = new System.Drawing.Point(12, 210); + this.rememberInSettingsCb.Name = "rememberInSettingsCb"; + this.rememberInSettingsCb.Size = new System.Drawing.Size(195, 19); + this.rememberInSettingsCb.TabIndex = 3; + this.rememberInSettingsCb.Text = "Remember this choice in Settings"; + this.rememberInSettingsCb.UseVisualStyleBackColor = true; + // + // abortBtn + // + this.abortBtn.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.abortBtn.Location = new System.Drawing.Point(197, 8); + this.abortBtn.MinimumSize = new System.Drawing.Size(75, 28); + this.abortBtn.Name = "abortBtn"; + this.abortBtn.Size = new System.Drawing.Size(75, 28); + this.abortBtn.TabIndex = 4; + this.abortBtn.Text = "Abort"; + this.abortBtn.UseVisualStyleBackColor = true; + this.abortBtn.Click += new System.EventHandler(this.AbortBtn_Click); + // + // retryBtn + // + this.retryBtn.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.retryBtn.Location = new System.Drawing.Point(278, 8); + this.retryBtn.MinimumSize = new System.Drawing.Size(75, 28); + this.retryBtn.Name = "retryBtn"; + this.retryBtn.Size = new System.Drawing.Size(75, 28); + this.retryBtn.TabIndex = 5; + this.retryBtn.Text = "Retry"; + this.retryBtn.UseVisualStyleBackColor = true; + this.retryBtn.Click += new System.EventHandler(this.RetryBtn_Click); + // + // ignoreBtn + // + this.ignoreBtn.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.ignoreBtn.Location = new System.Drawing.Point(359, 8); + this.ignoreBtn.MinimumSize = new System.Drawing.Size(75, 28); + this.ignoreBtn.Name = "ignoreBtn"; + this.ignoreBtn.Size = new System.Drawing.Size(75, 28); + this.ignoreBtn.TabIndex = 6; + this.ignoreBtn.Text = "Ignore"; + this.ignoreBtn.UseVisualStyleBackColor = true; + this.ignoreBtn.Click += new System.EventHandler(this.IgnoreBtn_Click); + // + // buttonPanel + // + this.buttonPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.buttonPanel.Controls.Add(this.abortBtn); + this.buttonPanel.Controls.Add(this.retryBtn); + this.buttonPanel.Controls.Add(this.ignoreBtn); + this.buttonPanel.Location = new System.Drawing.Point(0, 240); + this.buttonPanel.Name = "buttonPanel"; + this.buttonPanel.Size = new System.Drawing.Size(484, 45); + this.buttonPanel.TabIndex = 7; + // + // BadBookActionDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + this.ClientSize = new System.Drawing.Size(484, 285); + this.Controls.Add(this.buttonPanel); + this.Controls.Add(this.rememberInSettingsCb); + this.Controls.Add(this.applyToAllCb); + this.Controls.Add(this.messageLbl); + this.Controls.Add(this.pictureBox); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "BadBookActionDialog"; + this.ShowInTaskbar = true; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Skip this book?"; + ((System.ComponentModel.ISupportInitialize)(this.pictureBox)).EndInit(); + this.buttonPanel.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private System.Windows.Forms.PictureBox pictureBox; + private System.Windows.Forms.Label messageLbl; + private System.Windows.Forms.CheckBox applyToAllCb; + private System.Windows.Forms.CheckBox rememberInSettingsCb; + private System.Windows.Forms.Button abortBtn; + private System.Windows.Forms.Button retryBtn; + private System.Windows.Forms.Button ignoreBtn; + private System.Windows.Forms.Panel buttonPanel; + } +} diff --git a/Source/LibationWinForms/Dialogs/BadBookActionDialog.cs b/Source/LibationWinForms/Dialogs/BadBookActionDialog.cs new file mode 100644 index 00000000..9af47d25 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/BadBookActionDialog.cs @@ -0,0 +1,68 @@ +using LibationUiBase; +using LibationUiBase.Forms; +using System; +using System.Drawing; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace LibationWinForms.Dialogs; + +public partial class BadBookActionDialog : Form +{ + public BadBookDialogResult Result { get; private set; } = new(LibationUiBase.Forms.DialogResult.Retry, false, false); + + public BadBookActionDialog() + { + InitializeComponent(); + this.SetLibationIcon(); + } + + public BadBookActionDialog(string message, string caption) : this() + { + Text = caption; + messageLbl.Text = message; + + SizeChanged += (_, _) => AdjustLayout(); + Shown += (_, _) => AdjustLayout(); + messageLbl.SizeChanged += (_, _) => AdjustLayout(); + AdjustLayout(); + } + + private void AdjustLayout() + { + const int margin = 12; + const int gap = 10; + const int messageLeft = 50; + + var messageWidth = ClientSize.Width - messageLeft - margin; + messageLbl.Left = messageLeft; + messageLbl.Top = margin; + messageLbl.Width = messageWidth; + messageLbl.MaximumSize = new Size(messageWidth, 0); + + applyToAllCb.Top = messageLbl.Bottom + gap; + rememberInSettingsCb.Top = applyToAllCb.Bottom + gap; + buttonPanel.Top = rememberInSettingsCb.Bottom + gap; + buttonPanel.Width = ClientSize.Width; + + var desiredHeight = buttonPanel.Bottom + margin; + if (ClientSize.Height != desiredHeight) + ClientSize = new Size(ClientSize.Width, desiredHeight); + } + + private void CloseWith(LibationUiBase.Forms.DialogResult action) + { + Result = new BadBookDialogResult(action, applyToAllCb.Checked, rememberInSettingsCb.Checked); + base.DialogResult = System.Windows.Forms.DialogResult.OK; + Close(); + } + + private void AbortBtn_Click(object sender, EventArgs e) + => CloseWith(LibationUiBase.Forms.DialogResult.Abort); + + private void RetryBtn_Click(object sender, EventArgs e) + => CloseWith(LibationUiBase.Forms.DialogResult.Retry); + + private void IgnoreBtn_Click(object sender, EventArgs e) + => CloseWith(LibationUiBase.Forms.DialogResult.Ignore); +} diff --git a/Source/LibationWinForms/Form1.Debug.cs b/Source/LibationWinForms/Form1.Debug.cs new file mode 100644 index 00000000..4b9594c6 --- /dev/null +++ b/Source/LibationWinForms/Form1.Debug.cs @@ -0,0 +1,55 @@ +#if DEBUG +using DataLayer; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace LibationWinForms; + +public partial class Form1 +{ + private void Configure_DebugMenu() + { + var simulateItem = new ToolStripMenuItem("Simulate bad book failures (test dialog)..."); + simulateItem.Click += async (_, _) => await SimulateBadBookFailuresAsync(); + + // Insert before Tour; toolStripSeparator2 above Tour already provides the divider. + var insertIndex = settingsToolStripMenuItem.DropDownItems.IndexOf(tourToolStripMenuItem); + if (insertIndex < 0) + insertIndex = settingsToolStripMenuItem.DropDownItems.Count; + + settingsToolStripMenuItem.DropDownItems.Insert(insertIndex, simulateItem); + } + + private async Task SimulateBadBookFailuresAsync() + { + var books = productsDisplay.GetVisible().Take(5).ToArray(); + if (books.Length == 0) + { + MessageBox.Show( + this, + "No books are visible in the grid.\n\nClear your filter or widen it, then try again.", + "Test bad book dialog", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + return; + } + + var confirm = MessageBox.Show( + this, + $"Queue {books.Length} visible book(s) with simulated failures?\n\n" + + "No files will be downloaded. Each book will immediately show the bad-book error dialog.\n\n" + + "Set error handling to \"Ask each time\" in Settings > Download/Decrypt before testing.", + "Test bad book dialog", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question, + MessageBoxDefaultButton.Button1); + + if (confirm != System.Windows.Forms.DialogResult.Yes) + return; + + processBookQueue1.ViewModel.QueueSimulatedBadBookFailures(books); + SetQueueCollapseState(false); + } +} +#endif diff --git a/Source/LibationWinForms/Form1.cs b/Source/LibationWinForms/Form1.cs index c43d3dd2..b5c84b64 100644 --- a/Source/LibationWinForms/Form1.cs +++ b/Source/LibationWinForms/Form1.cs @@ -50,6 +50,9 @@ public partial class Form1 : Form Configure_Upgrade(); // misc which belongs in winforms app but doesn't have a UI element Configure_NonUI(); +#if DEBUG + Configure_DebugMenu(); +#endif // Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1' { diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs index 7ce5be0b..799f8b30 100644 --- a/Source/LibationWinForms/Program.cs +++ b/Source/LibationWinForms/Program.cs @@ -48,6 +48,7 @@ static class Program // Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations(); LibationUiBase.Forms.MessageBoxBase.ShowAsyncImpl = ShowMessageBox; + BadBookActionDialogBase.ShowAsyncImpl = ShowBadBookActionDialog; // do this as soon as possible (post-config) RunSetupIfNeededAsync(config); @@ -127,6 +128,22 @@ static class Program } #endregion; + #region Bad Book Action Dialog Handler for LibationUiBase + static Task ShowBadBookActionDialog(object? owner, string message, string caption) + { + Func showDialog = () => + { + BadBookDialogResult result = new(LibationUiBase.Forms.DialogResult.Retry, false, false); + using var dialog = new BadBookActionDialog(message, caption); + dialog.ShowDialog(owner as IWin32Window ?? form1); + return dialog.Result; + }; + + var result = form1 is null ? showDialog() : form1.Invoke(showDialog); + return Task.FromResult(result); + } + #endregion; + private static void SetThemeColor(Configuration config) { var theme = config.ThemeVariant switch