Merge pull request #1868 from rmcrackan/rmcrackan/1865-bad-book-setting

#1865 - Add in-dialog "Apply to all" and "Remember in Settings"
This commit is contained in:
rmcrackan
2026-06-12 10:10:29 -04:00
committed by GitHub
15 changed files with 651 additions and 11 deletions

View File

@@ -0,0 +1,30 @@
using DataLayer;
using Dinah.Core.ErrorHandling;
using LibationFileManager;
using System.Threading.Tasks;
namespace FileLiberator;
/// <summary>
/// Instantly fails processing so the bad-book error dialog can be tested without a real download failure.
/// </summary>
public class SimulateBadBookFailure : Processable, IProcessable<SimulateBadBookFailure>
{
public override string Name => "Simulate Bad Book Failure";
public override bool Validate(LibraryBook libraryBook) => true;
public override Task<StatusHandler> 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 };
}

View File

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

View File

@@ -0,0 +1,40 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="320"
MinWidth="400" MaxWidth="520"
SizeToContent="Height"
CanMaximize="False"
CanMinimize="False"
x:Class="LibationAvalonia.Dialogs.BadBookActionDialog"
ShowInTaskbar="True"
WindowStartupLocation="CenterOwner">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" Margin="5,10,10,10">
<Image Grid.Row="0" Grid.Column="0" Height="32" Width="32" Margin="5,0,5,0"
VerticalAlignment="Top" Stretch="Uniform" Source="/Assets/MBIcons/Question_64.png"/>
<TextBlock Grid.Row="0" Grid.Column="1" Name="messageTextBlock" Margin="5,0,0,0" MinWidth="300"
TextWrapping="Wrap" HorizontalAlignment="Stretch" VerticalAlignment="Top"
FontSize="12"/>
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Margin="10,10,10,10" Spacing="6">
<CheckBox Name="applyToAllCheckBox" Content="Apply to all remaining books in this queue"/>
<CheckBox Name="rememberInSettingsCheckBox" Content="Remember this choice in Settings"/>
</StackPanel>
<DockPanel Height="45" Grid.Row="2" Grid.ColumnSpan="2" Background="{DynamicResource SystemChromeMediumLowColor}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" DockPanel.Dock="Bottom">
<Button MinWidth="75" MinHeight="28" Name="abortButton" Click="AbortButton_Click" Margin="5">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="Abort"/>
</Button>
<Button MinWidth="75" MinHeight="28" Name="retryButton" Click="RetryButton_Click" Margin="5">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="Retry"/>
</Button>
<Button MinWidth="75" MinHeight="28" Name="ignoreButton" Click="IgnoreButton_Click" Margin="5">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="Ignore"/>
</Button>
</StackPanel>
</DockPanel>
</Grid>
</Window>

View File

@@ -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<BadBookDialogResult> 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);
}
}
}

View File

@@ -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

View File

@@ -46,8 +46,41 @@ public partial class MainWindow : ReactiveWindow<MainVM>
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)
{

View File

@@ -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<BadBookDialogResult> 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<BadBookDialogResult> 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<BadBookDialogResult> Show(string message, string caption)
=> ShowAsyncImpl(null, message, caption);
public static Task<BadBookDialogResult> Show(object? owner, string message, string caption)
=> ShowAsyncImpl(owner, message, caption);
}

View File

@@ -0,0 +1,10 @@
using LibationFileManager;
namespace LibationUiBase.ProcessQueue;
public class BadBookSessionContext
{
public Configuration.BadBookAction? Override { get; set; }
public void Reset() => Override = null;
}

View File

@@ -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
/// <summary> A series of Processable actions to perform on this book </summary>
protected Queue<Func<Processable>> 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<DownloadPdf>();
public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public ProcessBookViewModel AddConvertToMp3() => AddProcessable<ConvertToMp3>();
public ProcessBookViewModel AddSimulateBadBookFailure() => AddProcessable<SimulateBadBookFailure>();
private ProcessBookViewModel AddProcessable<T>() where T : Processable, IProcessable<T>
{
@@ -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
}

View File

@@ -21,6 +21,7 @@ public class ProcessQueueViewModel : ReactiveObject
{
public ObservableCollection<LogEntry> LogEntries { get; } = new();
public TrackedQueue<ProcessBookViewModel> 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;
}
/// <summary>
/// Queues visible books with an instant simulated failure for testing the bad-book error dialog.
/// Does not download or modify files.
/// </summary>
public void QueueSimulatedBadBookFailures(IList<LibraryBook> libraryBooks, Configuration? config = null, int maxBooks = 5)
{
config ??= Configuration.Instance;
if (libraryBooks.Count == 0)
return;
RunOnQueueUiThread(() => addSimulatedBadBookFailuresCore(libraryBooks, config, maxBooks));
}
private void addSimulatedBadBookFailuresCore(IList<LibraryBook> 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<bool> QueueDownloadDecryptAsync(IList<LibraryBook> 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<LibraryBook> 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<LibraryBook> 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<ProcessBookViewModel> 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;

View File

@@ -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;
}
}

View File

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

View File

@@ -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

View File

@@ -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'
{

View File

@@ -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<BadBookDialogResult> ShowBadBookActionDialog(object? owner, string message, string caption)
{
Func<BadBookDialogResult> 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