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