Added login dialogs

This commit is contained in:
Michael Bucari-Tovo 2022-07-22 18:25:47 -06:00
parent 503e1e143e
commit 9ecb32c3d2
40 changed files with 1100 additions and 169 deletions

View File

@ -1,5 +1,7 @@
using Avalonia.Media; using Avalonia.Media;
using System; using System;
using System.Threading;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI namespace LibationWinForms.AvaloniaUI
{ {
@ -13,5 +15,14 @@ namespace LibationWinForms.AvaloniaUI
return brush; return brush;
return defaultBrush; return defaultBrush;
} }
public static T ShowDialogSynchronously<T>(this Avalonia.Controls.Window window, Avalonia.Controls.Window owner)
{
using var source = new CancellationTokenSource();
var dialogTask = window.ShowDialog<T>(owner);
dialogTask.ContinueWith(t => source.Cancel(), TaskScheduler.FromCurrentSynchronizationContext());
Avalonia.Threading.Dispatcher.UIThread.MainLoop(source.Token);
return dialogTask.Result;
}
} }
} }

View File

@ -116,10 +116,12 @@ namespace LibationWinForms.AvaloniaUI.Controls
private void setDirectory() private void setDirectory()
{ {
Directory var path1
= customStates.CustomChecked ? customStates.CustomDir = customStates.CustomChecked ? customStates.CustomDir
: directorySelectControl.SelectedDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : directorySelectControl.SelectedDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
: Configuration.GetKnownDirectoryPath(directorySelectControl.SelectedDirectory); : Configuration.GetKnownDirectoryPath(directorySelectControl.SelectedDirectory);
Directory
= System.IO.Path.Combine(path1 ?? string.Empty, SubDirectory);
} }

View File

@ -26,7 +26,6 @@ namespace LibationWinForms.AvaloniaUI
Continue = 11 Continue = 11
} }
public enum MessageBoxIcon public enum MessageBoxIcon
{ {
None = 0, None = 0,
@ -39,6 +38,7 @@ namespace LibationWinForms.AvaloniaUI
Asterisk = 64, Asterisk = 64,
Information = 64 Information = 64
} }
public enum MessageBoxButtons public enum MessageBoxButtons
{ {
OK, OK,
@ -74,10 +74,8 @@ namespace LibationWinForms.AvaloniaUI
/// -or- /// -or-
/// <paramref name="defaultButton" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxDefaultButton" />.</exception> /// <paramref name="defaultButton" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxDefaultButton" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception> /// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{ => ShowCoreAsync(null, text, caption, buttons, icon, defaultButton);
return await ShowCore(null, text, caption, buttons, icon, defaultButton);
}
/// <summary>Displays a message box with specified text, caption, buttons, and icon.</summary> /// <summary>Displays a message box with specified text, caption, buttons, and icon.</summary>
@ -90,10 +88,8 @@ namespace LibationWinForms.AvaloniaUI
/// -or- /// -or-
/// The <paramref name="icon" /> parameter specified is not a member of <see cref="T:System.Windows.Forms.MessageBoxIcon" />.</exception> /// The <paramref name="icon" /> parameter specified is not a member of <see cref="T:System.Windows.Forms.MessageBoxIcon" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception> /// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
{ => ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
return await ShowCore(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box with specified text, caption, and buttons.</summary> /// <summary>Displays a message box with specified text, caption, and buttons.</summary>
@ -103,28 +99,22 @@ namespace LibationWinForms.AvaloniaUI
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns> /// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
/// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">The <paramref name="buttons" /> parameter specified is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.</exception> /// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">The <paramref name="buttons" /> parameter specified is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception> /// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons) public static DialogResult Show(string text, string caption, MessageBoxButtons buttons)
{ => ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
return await ShowCore(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box with specified text and caption.</summary> /// <summary>Displays a message box with specified text and caption.</summary>
/// <param name="text">The text to display in the message box.</param> /// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param> /// <param name="caption">The text to display in the title bar of the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns> /// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
public static async Task<DialogResult> Show(string text, string caption) public static DialogResult Show(string text, string caption)
{ => ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
return await ShowCore(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box with specified text.</summary> /// <summary>Displays a message box with specified text.</summary>
/// <param name="text">The text to display in the message box.</param> /// <param name="text">The text to display in the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns> /// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
public static async Task<DialogResult> Show(string text) public static DialogResult Show(string text)
{ => ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
return await ShowCore(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box in front of the specified object and with the specified text, caption, buttons, icon, default button, and options.</summary> /// <summary>Displays a message box in front of the specified object and with the specified text, caption, buttons, icon, default button, and options.</summary>
@ -146,10 +136,9 @@ namespace LibationWinForms.AvaloniaUI
/// <exception cref="T:System.ArgumentException"> /// <exception cref="T:System.ArgumentException">
/// -or- /// -or-
/// <paramref name="buttons" /> specified an invalid combination of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.</exception> /// <paramref name="buttons" /> specified an invalid combination of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.</exception>
public static async Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) public static DialogResult Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{ => ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton);
return await ShowCore(owner, text, caption, buttons, icon, defaultButton);
}
/// <summary>Displays a message box in front of the specified object and with the specified text, caption, buttons, and icon.</summary> /// <summary>Displays a message box in front of the specified object and with the specified text, caption, buttons, and icon.</summary>
@ -164,10 +153,8 @@ namespace LibationWinForms.AvaloniaUI
/// -or- /// -or-
/// <paramref name="icon" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxIcon" />.</exception> /// <paramref name="icon" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxIcon" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception> /// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) public static DialogResult Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
{ => ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
return await ShowCore(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box in front of the specified object and with the specified text, caption, and buttons.</summary> /// <summary>Displays a message box in front of the specified object and with the specified text, caption, and buttons.</summary>
/// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param> /// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param>
@ -178,35 +165,29 @@ namespace LibationWinForms.AvaloniaUI
/// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException"> /// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">
/// <paramref name="buttons" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.</exception> /// <paramref name="buttons" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception> /// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons) public static DialogResult Show(Window owner, string text, string caption, MessageBoxButtons buttons)
{ => ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
return await ShowCore(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box in front of the specified object and with the specified text and caption.</summary> /// <summary>Displays a message box in front of the specified object and with the specified text and caption.</summary>
/// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param> /// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param>
/// <param name="text">The text to display in the message box.</param> /// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param> /// <param name="caption">The text to display in the title bar of the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns> /// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
public static async Task<DialogResult> Show(Window owner, string text, string caption) public static DialogResult Show(Window owner, string text, string caption)
{ => ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
return await ShowCore(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box in front of the specified object and with the specified text.</summary> /// <summary>Displays a message box in front of the specified object and with the specified text.</summary>
/// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param> /// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param>
/// <param name="text">The text to display in the message box.</param> /// <param name="text">The text to display in the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns> /// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
public static async Task<DialogResult> Show(Window owner, string text) public static DialogResult Show(Window owner, string text)
{ => ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
return await ShowCore(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
public static async Task VerboseLoggingWarning_ShowIfTrue() public static async Task VerboseLoggingWarning_ShowIfTrue()
{ {
// when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured // when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured
if (Serilog.Log.Logger.IsVerboseEnabled()) if (Serilog.Log.Logger.IsVerboseEnabled())
await Show(@" Show(@"
Warning: verbose logging is enabled. Warning: verbose logging is enabled.
This should be used for debugging only. It creates many This should be used for debugging only. It creates many
@ -219,7 +200,7 @@ Libation.
".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning); ".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
} }
public static async Task<DialogResult> ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1) public static DialogResult ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
{ {
if (libraryBooks is null || !libraryBooks.Any()) if (libraryBooks is null || !libraryBooks.Any())
return DialogResult.Cancel; return DialogResult.Cancel;
@ -234,7 +215,7 @@ Libation.
= string.Format(format, $"{thisThese} {count} {bookBooks}") = string.Format(format, $"{thisThese} {count} {bookBooks}")
+ $"\r\n\r\n{titlesAgg}"; + $"\r\n\r\n{titlesAgg}";
return await ShowCore(owner, return ShowCoreAsync(owner,
message, message,
title, title,
MessageBoxButtons.YesNo, MessageBoxButtons.YesNo,
@ -263,18 +244,11 @@ Libation.
var form = new MessageBoxAlertAdminDialog(text, caption, exception); var form = new MessageBoxAlertAdminDialog(text, caption, exception);
await DisplayWindow(form, owner); DisplayWindow(form, owner);
} }
private static async Task<DialogResult> ShowCore(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) private static DialogResult ShowCoreAsync(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{
if (Avalonia.Threading.Dispatcher.UIThread.CheckAccess())
return await ShowCore2(owner, message, caption, buttons, icon, defaultButton);
else
return await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => ShowCore2(owner, message, caption, buttons, icon, defaultButton));
}
private static async Task<DialogResult> ShowCore2(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{ {
var dialog = new MessageBoxWindow(); var dialog = new MessageBoxWindow();
@ -307,15 +281,15 @@ Libation.
dialog.Height = dialog.MinHeight; dialog.Height = dialog.MinHeight;
dialog.Width = dialog.MinWidth; dialog.Width = dialog.MinWidth;
return await DisplayWindow(dialog, owner); return DisplayWindow(dialog, owner);
} }
private static async Task<DialogResult> DisplayWindow(Window toDisplay, Window owner) private static DialogResult DisplayWindow(Window toDisplay, Window owner)
{ {
if (owner is null) if (owner is null)
{ {
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
return await toDisplay.ShowDialog<DialogResult>(desktop.MainWindow); return toDisplay.ShowDialogSynchronously<DialogResult>(desktop.MainWindow);
} }
else else
{ {
@ -329,7 +303,7 @@ Libation.
}; };
window.Show(); window.Show();
var result = await toDisplay.ShowDialog<DialogResult>(window); var result = toDisplay.ShowDialogSynchronously<DialogResult>(window);
window.Close(); window.Close();
return result; return result;
} }
@ -337,7 +311,7 @@ Libation.
} }
else else
{ {
return await toDisplay.ShowDialog<DialogResult>(owner); return toDisplay.ShowDialogSynchronously<DialogResult>(owner);
} }
} }

View File

@ -346,7 +346,7 @@ $@" Title: {libraryBook.Book.Title}
} }
// if null then ask user // if null then ask user
dialogResult ??= await MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton); dialogResult ??= MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
if (dialogResult == DialogResult.Abort) if (dialogResult == DialogResult.Abort)
return ProcessBookResult.FailedAbort; return ProcessBookResult.FailedAbort;

View File

@ -243,7 +243,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
return; return;
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList(); var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = await MessageBox.ShowConfirmationDialog( var result = MessageBox.ShowConfirmationDialog(
null, null,
libraryBooks, libraryBooks,
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?", $"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
@ -307,7 +307,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
.Select(lbe => lbe.LibraryBook) .Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated()); .Where(lb => !lb.Book.HasLiberated());
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts); var removedBooks = await LibraryCommands.FindInactiveBooks(Views.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, lib, accounts);
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList(); var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();

View File

@ -56,13 +56,17 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
// only persist in 'save' step // only persist in 'save' step
using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts; var accounts = persister.AccountsSettings.Accounts;
if (!accounts.Any()) if (accounts.Any())
return; {
DataContext = this;
foreach (var account in accounts) foreach (var account in accounts)
AddAccountToGrid(account); AddAccountToGrid(account);
}
DataContext = this;
addBlankAccount();
}
private void addBlankAccount()
{
var newBlank = new AccountDto(); var newBlank = new AccountDto();
newBlank.PropertyChanged += AccountDto_PropertyChanged; newBlank.PropertyChanged += AccountDto_PropertyChanged;
@ -89,9 +93,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
if (Accounts.Any(a => a.IsDefault)) if (Accounts.Any(a => a.IsDefault))
return; return;
var newBlank = new AccountDto(); addBlankAccount();
newBlank.PropertyChanged += AccountDto_PropertyChanged;
Accounts.Insert(Accounts.Count, newBlank);
} }
public void DeleteButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void DeleteButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@ -134,7 +136,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
if (persister.AccountsSettings.Accounts.Any(a => a.AccountId == account.AccountId && a.IdentityTokens.Locale.Name == account.Locale.Name)) if (persister.AccountsSettings.Accounts.Any(a => a.AccountId == account.AccountId && a.IdentityTokens.Locale.Name == account.Locale.Name))
{ {
await MessageBox.Show(this, $"An account with that account id and country already exists.\r\n\r\nAccount ID: {account.AccountId}\r\nCountry: {account.Locale.Name}", "Cannot Add Duplicate Account"); MessageBox.Show(this, $"An account with that account id and country already exists.\r\n\r\nAccount ID: {account.AccountId}\r\nCountry: {account.Locale.Name}", "Cannot Add Duplicate Account");
return; return;
} }
@ -144,7 +146,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
} }
catch (Exception ex) catch (Exception ex)
{ {
await MessageBox.ShowAdminAlert( MessageBox.ShowAdminAlert(
this, this,
$"An error occurred while importing an account from:\r\n{filePath[0]}\r\n\r\nIs the file encrypted?", $"An error occurred while importing an account from:\r\n{filePath[0]}\r\n\r\nIs the file encrypted?",
"Error Importing Account", "Error Importing Account",
@ -163,7 +165,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
try try
{ {
if (!await inputIsValid()) if (!inputIsValid())
return; return;
// without transaction, accounts persister will write ANY EDIT immediately to file // without transaction, accounts persister will write ANY EDIT immediately to file
@ -190,8 +192,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private void persist(AccountsSettings accountsSettings) private void persist(AccountsSettings accountsSettings)
{ {
var existingAccounts = accountsSettings.Accounts; var existingAccounts = accountsSettings.Accounts;
@ -222,7 +222,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
: dto.AccountName.Trim(); : dto.AccountName.Trim();
} }
} }
private async Task<bool> inputIsValid() private bool inputIsValid()
{ {
foreach (var dto in Accounts.ToList()) foreach (var dto in Accounts.ToList())
{ {
@ -234,13 +234,13 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
if (string.IsNullOrWhiteSpace(dto.AccountId)) if (string.IsNullOrWhiteSpace(dto.AccountId))
{ {
await MessageBox.Show(this, "Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, "Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false; return false;
} }
if (string.IsNullOrWhiteSpace(dto.SelectedLocale?.Name)) if (string.IsNullOrWhiteSpace(dto.SelectedLocale?.Name))
{ {
await MessageBox.Show(this, "Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, "Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false; return false;
} }
} }
@ -260,7 +260,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
if (account.IdentityTokens?.IsValid != true) if (account.IdentityTokens?.IsValid != true)
{ {
await MessageBox.Show(this, "This account hasn't been authenticated yet. First scan your library to log into your account, then try exporting again.", "Account Not Authenticated"); MessageBox.Show(this, "This account hasn't been authenticated yet. First scan your library to log into your account, then try exporting again.", "Account Not Authenticated");
return; return;
} }
@ -283,11 +283,11 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
File.WriteAllText(fileName, jsonText); File.WriteAllText(fileName, jsonText);
await MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{fileName}", "Success!"); MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{fileName}", "Success!");
} }
catch (Exception ex) catch (Exception ex)
{ {
await MessageBox.ShowAdminAlert( MessageBox.ShowAdminAlert(
this, this,
$"An error occurred while exporting account:\r\n{account.AccountName}", $"An error occurred while exporting account:\r\n{account.AccountName}",
"Error Exporting Account", "Error Exporting Account",

View File

@ -17,13 +17,30 @@
<Setter Property="BorderThickness" Value="2" /> <Setter Property="BorderThickness" Value="2" />
</Style> </Style>
</Grid.Styles> </Grid.Styles>
<Grid ColumnDefinitions="Auto,*" Margin="10,10,10,0"> <Grid ColumnDefinitions="Auto,*" RowDefinitions="*,Auto" Margin="10,10,10,0">
<Panel VerticalAlignment="Top" Margin="5" Background="LightGray" Width="80" Height="80" > <Panel VerticalAlignment="Top" Margin="5" Background="LightGray" Width="80" Height="80" >
<Image Grid.Column="0" Width="80" Height="80" Source="{Binding Cover}" /> <Image Grid.Column="0" Width="80" Height="80" Source="{Binding Cover}" />
</Panel> </Panel>
<Panel Grid.Column="0" Grid.Row="1">
<Panel.Styles>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="Blue"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="TextDecorations" Value="Underline"/>
</Style>
</Panel.Styles>
<TextBlock
Margin="10" TextWrapping="Wrap" TextAlignment="Center"
Tapped="GoToAudible_Tapped"
Text="Open in&#xa;Audible&#xa;(Browser)" />
</Panel>
<TextBox <TextBox
Grid.Column="1" Grid.Column="1"
Grid.Row="0"
Grid.RowSpan="2"
TextWrapping="Wrap" TextWrapping="Wrap"
Margin="5" Margin="5"
FontSize="12" FontSize="12"
@ -43,7 +60,7 @@
<TextBox Margin="0,5,0,5" <TextBox Margin="0,5,0,5"
MinHeight="25" MinHeight="25"
FontSize="12" FontSize="12" Name="tagsTbox"
Text="{Binding Tags, Mode=TwoWay}"/> Text="{Binding Tags, Mode=TwoWay}"/>
</StackPanel> </StackPanel>
</controls:GroupBox> </controls:GroupBox>

View File

@ -4,6 +4,7 @@ using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using DataLayer; using DataLayer;
using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using LibationWinForms.AvaloniaUI.ViewModels; using LibationWinForms.AvaloniaUI.ViewModels;
using System.Collections.Generic; using System.Collections.Generic;
@ -34,6 +35,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
public BookDetailsDialog() public BookDetailsDialog()
{ {
InitializeComponent(); InitializeComponent();
ControlToFocusOnShow = this.Find<TextBox>(nameof(tagsTbox));
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
@ -46,13 +48,18 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
LibraryBook = libraryBook; LibraryBook = libraryBook;
} }
protected override void SaveAndClose() protected override void SaveAndClose()
{ {
SaveButton_Clicked(null, null); SaveButton_Clicked(null, null);
base.SaveAndClose(); base.SaveAndClose();
} }
public void GoToAudible_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var locale = AudibleApi.Localization.Get(_libraryBook.Book.Locale);
var link = $"https://www.audible.{locale.TopDomain}/pd/{_libraryBook.Book.AudibleProductId}";
Go.To.Url(link);
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {

View File

@ -38,7 +38,7 @@
Content="Reset to Default" Content="Reset to Default"
Click="ResetButton_Click" /> Click="ResetButton_Click" />
</Grid> </Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,*"> <Grid Grid.Row="1" ColumnDefinitions="Auto,*">
<Border <Border
Grid.Row="0" Grid.Row="0"
@ -62,7 +62,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="Description"> <DataGridTemplateColumn Width="Auto" Header="Description">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<TextPresenter <TextPresenter
@ -80,10 +80,9 @@
<Grid <Grid
Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Margin="5" Margin="5"
RowDefinitions="Auto,*,80"> RowDefinitions="Auto,*,80" HorizontalAlignment="Stretch">
<TextBlock <TextBlock
Margin="5,5,5,10" Margin="5,5,5,10"

View File

@ -19,7 +19,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{ {
class BracketEscapeConverter : IValueConverter class BracketEscapeConverter : IValueConverter
{ {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
if (value is string str && str[0] != '<' && str[^1] != '>') if (value is string str && str[0] != '<' && str[^1] != '>')
@ -68,7 +67,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
} }
protected override async Task SaveAndCloseAsync() protected override async Task SaveAndCloseAsync()
{ {
if (!await _viewModel.Validate()) if (!_viewModel.Validate())
return; return;
TemplateText = _viewModel.workingTemplateText; TemplateText = _viewModel.workingTemplateText;
@ -119,7 +118,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
public void resetTextBox(string value) => workingTemplateText = value; public void resetTextBox(string value) => workingTemplateText = value;
public async Task<bool> Validate() public bool Validate()
{ {
if (template.IsValid(workingTemplateText)) if (template.IsValid(workingTemplateText))
return true; return true;
@ -127,7 +126,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
.GetErrors(workingTemplateText) .GetErrors(workingTemplateText)
.Select(err => $"- {err}") .Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}"); .Aggregate((a, b) => $"{a}\r\n{b}");
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false; return false;
} }
@ -232,11 +231,13 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
for(int i = 0; i < wordsSplit.Length; i++) for(int i = 0; i < wordsSplit.Length; i++)
{ {
var tb = new TextBlock(); var tb = new TextBlock
tb.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom; {
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
tb.Text = wordsSplit[i] + (i == wordsSplit.Length - 1 ? "" : " "); TextWrapping = TextWrapping.Wrap,
tb.FontWeight = item.Item2; Text = wordsSplit[i] + (i == wordsSplit.Length - 1 ? "" : " "),
FontWeight = item.Item2
};
WrapPanel.Children.Add(tb); WrapPanel.Children.Add(tb);
} }

View File

@ -64,7 +64,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, $"Failed to save picture to {fileName}"); Serilog.Log.Logger.Error(ex, $"Failed to save picture to {fileName}");
await MessageBox.Show(this, $"An error was encountered while trying to save the picture\r\n\r\n{ex.Message}", "Failed to save picture", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1); MessageBox.Show(this, $"An error was encountered while trying to save the picture\r\n\r\n{ex.Message}", "Failed to save picture", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
} }
} }

View File

@ -35,7 +35,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
if (!System.IO.Directory.Exists(libationDir)) if (!System.IO.Directory.Exists(libationDir))
{ {
await MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + libationDir, "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + libationDir, "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error);
return; return;
} }

View File

@ -0,0 +1,33 @@
<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="240" d:DesignHeight="120"
MinWidth="240" MinHeight="120"
MaxWidth="240" MaxHeight="120"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.Login.ApprovalNeededDialog"
Title="Approval Alert Detected"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock
Grid.Row="0"
Margin="10"
TextWrapping="Wrap"
Text="Amazon is sending you an email."/>
<TextBlock
Grid.Row="1" Margin="10,0,10,0"
TextWrapping="Wrap"
Text="Please press this button after you've approved the notification."/>
<Button
Grid.Row="2"
Margin="10"
VerticalAlignment="Bottom"
Padding="30,3,30,3"
Content="Approve"
Click="Approve_Click" />
</Grid>
</Window>

View File

@ -0,0 +1,30 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public partial class ApprovalNeededDialog : DialogWindow
{
public ApprovalNeededDialog()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Approve button clicked");
return base.SaveAndCloseAsync();
}
public async void Approve_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@ -0,0 +1,22 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using System;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public abstract class AvaloniaLoginBase
{
/// <returns>True if ShowDialog's DialogResult == OK</returns>
protected static bool ShowDialog(DialogWindow dialog)
{
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return false;
var result = dialog.ShowDialogSynchronously<DialogResult>(desktop.MainWindow);
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
return result == DialogResult.OK;
}
}
}

View File

@ -0,0 +1,55 @@
using System;
using AudibleApi;
using AudibleUtilities;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public class AvaloniaLoginCallback : AvaloniaLoginBase, ILoginCallback
{
private Account _account { get; }
public AvaloniaLoginCallback(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public string Get2faCode()
{
var dialog = new _2faCodeDialog();
if (ShowDialog(dialog))
return dialog.Code;
return null;
}
public string GetCaptchaAnswer(byte[] captchaImage)
{
var dialog = new CaptchaDialog(captchaImage);
if (ShowDialog(dialog))
return dialog.Answer;
return null;
}
public (string name, string value) GetMfaChoice(MfaConfig mfaConfig)
{
var dialog = new MfaDialog(mfaConfig);
if (ShowDialog(dialog))
return (dialog.SelectedName, dialog.SelectedValue);
return (null, null);
}
public (string email, string password) GetLogin()
{
var dialog = new LoginCallbackDialog(_account);
if (ShowDialog(dialog))
return (_account.AccountId, dialog.Password);
return (null, null);
}
public void ShowApprovalNeeded()
{
var dialog = new ApprovalNeededDialog();
ShowDialog(dialog);
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Threading.Tasks;
using AudibleApi;
using AudibleUtilities;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public class AvaloniaLoginChoiceEager : AvaloniaLoginBase, ILoginChoiceEager
{
/// <summary>Convenience method. Recommended when wiring up Winforms to <see cref="ApplicationServices.LibraryCommands.ImportAccountAsync"/></summary>
public static async Task<ApiExtended> ApiExtendedFunc(Account account) => await ApiExtended.CreateAsync(account, new AvaloniaLoginChoiceEager(account));
public ILoginCallback LoginCallback { get; private set; }
private Account _account { get; }
public AvaloniaLoginChoiceEager(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
LoginCallback = new AvaloniaLoginCallback(_account);
}
public ChoiceOut Start(ChoiceIn choiceIn)
{
var dialog = new LoginChoiceEagerDialog(_account);
if (!ShowDialog(dialog))
return null;
switch (dialog.LoginMethod)
{
case LoginMethod.Api:
return ChoiceOut.WithApi(dialog.Account.AccountId, dialog.Password);
case LoginMethod.External:
{
var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return ShowDialog(externalDialog)
? ChoiceOut.External(externalDialog.ResponseUrl)
: null;
}
default:
throw new Exception($"Unknown {nameof(LoginMethod)} value");
}
}
}
}

View File

@ -0,0 +1,54 @@
<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="220" d:DesignHeight="180"
MinWidth="220" MinHeight="180"
MaxWidth="220" MaxHeight="180"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.Login.CaptchaDialog"
Title="CAPTCHA"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid
RowDefinitions="Auto,Auto,*"
ColumnDefinitions="Auto,*">
<Panel
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="10"
MinWidth="200"
MinHeight="70"
Background="LightGray">
<Image
Stretch="None"
Source="{Binding CaptchaImage}" />
</Panel>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="10,0,10,0"
VerticalAlignment="Center"
Text="CAPTCHA&#xa;answer:" />
<TextBox
Grid.Row="1"
Grid.Column="1"
Margin="10,0,10,0" Text="{Binding Answer}" />
<Button
Grid.Row="2"
Grid.Column="1"
Margin="10"
Padding="0,5,0,5"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Content="Submit"
Click="Submit_Click" />
</Grid>
</Window>

View File

@ -0,0 +1,40 @@
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using System.IO;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public partial class CaptchaDialog : DialogWindow
{
public string Answer { get; set; }
public Bitmap CaptchaImage { get; }
public CaptchaDialog()
{
InitializeComponent();
}
public CaptchaDialog(byte[] captchaImage) :this()
{
using var ms = new MemoryStream(captchaImage);
CaptchaImage = new Bitmap(ms);
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });
return base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@ -0,0 +1,38 @@
<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="300" d:DesignHeight="120"
MinWidth="300" MinHeight="120"
Width="300" Height="120"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.Login.LoginCallbackDialog"
Title="Audible Login"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Text="Locale: " />
<TextBlock Text="{Binding Account.Locale.Name}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<TextBlock Text="Username: " />
<TextBlock Text="{Binding Account.AccountId}" />
</StackPanel>
<Grid Margin="0,5,0,5" Grid.Row="2" Grid.Column="0" ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="Password: " />
<TextBox Grid.Column="1" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" />
</Grid>
<Button
Grid.Row="3"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Padding="30,5,30,5"
Content="Submit"
Click="Submit_Click"/>
</Grid>
</Window>

View File

@ -0,0 +1,50 @@
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public partial class LoginCallbackDialog : DialogWindow
{
public Account Account { get; }
public string Password { get; set; }
public LoginCallbackDialog()
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
Account = accounts.FirstOrDefault();
DataContext = this;
}
}
public LoginCallbackDialog(Account account) : this()
{
Account = account;
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Account?.AccountId?.ToMask(), passwordLength = Password?.Length });
return base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@ -0,0 +1,66 @@
<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="350" d:DesignHeight="200"
MinWidth="350" MinHeight="200"
Width="350" Height="200"
WindowStartupLocation="CenterOwner"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.Login.LoginChoiceEagerDialog"
Title="Audible Login"
Icon="/AvaloniaUI/Assets/libation.ico" >
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<TextBlock Text="Locale: " />
<TextBlock Text="{Binding Account.Locale.Name}" />
</StackPanel>
<StackPanel
Grid.Row="1"
Orientation="Horizontal">
<TextBlock Text="Username: " />
<TextBlock Text="{Binding Account.AccountId}" />
</StackPanel>
<Grid
Grid.Row="2"
Grid.Column="0"
Margin="0,5,0,5"
ColumnDefinitions="Auto,*">
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="Password: " />
<TextBox
Grid.Column="1"
PasswordChar="*"
Text="{Binding Password, Mode=TwoWay}" />
</Grid>
<StackPanel
Grid.Row="3"
VerticalAlignment="Bottom">
<TextBlock
Foreground="Blue"
TextDecorations="Underline"
Tapped="ExternalLoginLink_Tapped"
Text="Or click here to log in with your browser." />
<TextBlock
TextWrapping="Wrap"
Text="This more advanced login is recommended if you're experiencing errors logging in the conventional way above or if you're not comfortable typing your password here." />
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,45 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public partial class LoginChoiceEagerDialog : DialogWindow
{
public Account Account { get; }
public string Password { get; set; }
public LoginMethod LoginMethod { get; private set; }
public LoginChoiceEagerDialog()
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
Account = accounts.FirstOrDefault();
DataContext = this;
}
}
public LoginChoiceEagerDialog(Account account):this()
{
Account = account;
DataContext = this;
}
public async void ExternalLoginLink_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
LoginMethod = LoginMethod.External;
await SaveAndCloseAsync();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,111 @@
<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="650" d:DesignHeight="500"
Width="650" Height="500"
WindowStartupLocation="CenterOwner"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.Login.LoginExternalDialog"
Title="Audible Login External"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="Auto,Auto,*,Auto,*" ColumnDefinitions="*" Margin="5">
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<TextBlock Text="Locale: " />
<TextBlock Text="{Binding Account.Locale.Name}" />
</StackPanel>
<StackPanel
Grid.Row="1"
Orientation="Horizontal">
<TextBlock Text="Username: " />
<TextBlock Text="{Binding Account.AccountId}" />
</StackPanel>
<Grid
Margin="0,5,0,5"
Grid.Row="2"
Grid.Column="0"
RowDefinitions="Auto,*,Auto"
ColumnDefinitions="*,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center"
Text="Paste this URL into your browser:" />
<TextBox
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
IsReadOnly="True"
TextWrapping="Wrap"
Text="{Binding ExternalLoginUrl}" />
<Button
Grid.Row="2"
Grid.Column="0"
Margin="0,5,0,0"
Content="Copy URL to Clipboard"
Click="CopyUrlToClipboard_Click" />
<Button
Grid.Row="2"
Grid.Column="1"
Margin="0,5,0,0"
Content="Launch in Browser"
Click="LaunchInBrowser_Click" />
</Grid>
<StackPanel
Grid.Row="3"
Orientation="Vertical"
VerticalAlignment="Bottom">
<TextBlock
TextWrapping="Wrap"
FontWeight="Bold"
Text="tl;dr : an ERROR on Amazon is GOOD. Sorry, I can't control their weird login" />
<TextBlock
TextWrapping="Wrap"
Text="Login with your Amazon/Audible credentials.
&#xa;After login is complete, your browser will show you an error page similar to:
&#xa; Looking for Something?
&#xa; We're sorry. The Web address you entered is not a functioning page on our site
&#xa;Don't worry -- this is ACTUALLY A SUCCESSFUL LOGIN.
&#xa;Copy the current url from your browser's address bar and paste it here:
" />
</StackPanel>
<Grid
Grid.Row="4"
Grid.Column="0"
Margin="0,5,0,5"
RowDefinitions="*,Auto">
<TextBox
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
TextWrapping="Wrap"
Text="{Binding ResponseUrl, Mode=TwoWay}" />
<Button
Grid.Row="1"
Margin="0,5,0,0"
Padding="30,3,30,3" HorizontalAlignment="Right"
Content="Submit"
Click="Submit_Click" />
</Grid>
</Grid>
</Window>

View File

@ -0,0 +1,71 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public partial class LoginExternalDialog : DialogWindow
{
public Account Account { get; }
public string ExternalLoginUrl { get; }
public string ResponseUrl { get; set; }
public LoginExternalDialog()
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
Account = accounts.FirstOrDefault();
ExternalLoginUrl = "ht" + "tps://us.audible.com/Test_url";
DataContext = this;
}
}
public LoginExternalDialog(Account account, string loginUrl):this()
{
Account = account;
ExternalLoginUrl = loginUrl;
DataContext = this;
}
public LoginExternalDialog(Account account)
{
Account = account;
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override async Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { ResponseUrl });
if (!Uri.TryCreate(ResponseUrl, UriKind.Absolute, out var result))
{
MessageBox.Show("Invalid response URL");
return;
}
await base.SaveAndCloseAsync();
}
public async void CopyUrlToClipboard_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await Application.Current.Clipboard.SetTextAsync(ExternalLoginUrl);
public void LaunchInBrowser_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> Go.To.Url(ExternalLoginUrl);
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@ -0,0 +1,18 @@
<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="400" d:DesignHeight="160"
MinWidth="400" MinHeight="160"
MaxWidth="400" MaxHeight="160"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.Login.MfaDialog"
Title="Two-Step Verification"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="*,Auto">
<StackPanel Grid.Row="0" Margin="10,0,10,10" Name="rbStackPanel" Orientation="Vertical"/>
<Button Grid.Row="1" Content="Submit" Margin="10" Padding="30,5,30,5" Click="Submit_Click" />
</Grid>
</Window>

View File

@ -0,0 +1,142 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Threading.Tasks;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Data;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public partial class MfaDialog : DialogWindow
{
public string SelectedName { get; private set; }
public string SelectedValue { get; private set; }
private RbValues Values { get; } = new();
public MfaDialog()
{
InitializeComponent();
if (Design.IsDesignMode)
{
var mfaConfig = new AudibleApi.MfaConfig { Title = "My title" };
mfaConfig.Buttons.Add(new() { Text = "Enter the OTP from the authenticator app", Name = "otpDeviceContext", Value = "aAbBcC=, TOTP" });
mfaConfig.Buttons.Add(new() { Text = "Send an SMS to my number ending with 123", Name = "otpDeviceContext", Value = "dDeEfE=, SMS" });
mfaConfig.Buttons.Add(new() { Text = "Call me on my number ending with 123", Name = "otpDeviceContext", Value = "dDeEfE=, VOICE" });
loadRadioButtons(mfaConfig);
}
}
public MfaDialog(AudibleApi.MfaConfig mfaConfig) : this()
{
loadRadioButtons(mfaConfig);
}
private void loadRadioButtons(AudibleApi.MfaConfig mfaConfig)
{
if (!string.IsNullOrWhiteSpace(mfaConfig.Title))
Title = mfaConfig.Title;
rbStackPanel = this.Find<StackPanel>(nameof(rbStackPanel));
foreach (var conf in mfaConfig.Buttons)
{
var rb = new RbValue(conf);
Values.AddButton(rb);
RadioButton radioButton = new()
{
Content = new TextBlock { Text = conf.Text },
Margin = new Thickness(0, 10, 0, 0),
};
radioButton.Bind(
RadioButton.IsCheckedProperty,
new Binding
{
Source = rb,
Path = nameof(rb.IsChecked)
});
rbStackPanel.Children.Add(radioButton);
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override async Task SaveAndCloseAsync()
{
var selected = Values.CheckedButton;
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new
{
text = selected?.Text,
name = selected?.Name,
value = selected?.Value
});
if (selected is null)
{
MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
SelectedName = selected.Name;
SelectedValue = selected.Value;
await base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
private class RbValue : ViewModels.ViewModelBase
{
private bool _isChecked;
public bool IsChecked
{
get => _isChecked;
set => this.RaiseAndSetIfChanged(ref _isChecked, value);
}
public AudibleApi.MfaConfigButton MfaConfigButton { get; }
public RbValue(AudibleApi.MfaConfigButton mfaConfig)
{
MfaConfigButton = mfaConfig;
}
}
private class RbValues
{
private List<RbValue> ButtonValues { get; } = new();
public AudibleApi.MfaConfigButton CheckedButton => ButtonValues.SingleOrDefault(rb => rb.IsChecked)?.MfaConfigButton;
public void AddButton(RbValue rbValue)
{
if (ButtonValues.Contains(rbValue))
return;
rbValue.PropertyChanged += RbValue_PropertyChanged;
rbValue.IsChecked = ButtonValues.Count == 0;
ButtonValues.Add(rbValue);
}
private void RbValue_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var button = sender as RbValue;
if (button.IsChecked)
{
foreach (var rb in ButtonValues.Where(rb => rb != button))
rb.IsChecked = false;
}
}
}
}
}

View File

@ -0,0 +1,32 @@
<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="140" d:DesignHeight="100"
MinWidth="140" MinHeight="100"
MaxWidth="140" MaxHeight="100"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.Login._2faCodeDialog"
Title="2FA Code"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock
Margin="5"
TextAlignment="Center"
Text="Enter 2FA Code" />
<TextBox
Margin="5,0,5,0"
Grid.Row="1"
Text="{Binding Code, Mode=TwoWay}" />
<Button
Margin="5"
Grid.Row="2"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Content="Submit"
Click="Submit_Click" />
</Grid>
</Window>

View File

@ -0,0 +1,33 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs.Login
{
public partial class _2faCodeDialog : DialogWindow
{
public string Code { get; set; }
public _2faCodeDialog()
{
InitializeComponent();
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Code });
return base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@ -28,7 +28,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
DataContext = this; DataContext = this;
} }
private async void GoToGithub_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e) private void GoToGithub_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
var url = "https://github.com/rmcrackan/Libation/issues"; var url = "https://github.com/rmcrackan/Libation/issues";
try try
@ -37,7 +37,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
} }
catch catch
{ {
await MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
} }
@ -56,7 +56,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
} }
catch catch
{ {
await MessageBox.Show($"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show($"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
} }

View File

@ -30,12 +30,16 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
protected override async Task SaveAndCloseAsync() protected override async Task SaveAndCloseAsync()
{ {
settingsDisp.SaveSettings(config); if (!settingsDisp.SaveSettings(config))
return;
await MessageBox.VerboseLoggingWarning_ShowIfTrue(); await MessageBox.VerboseLoggingWarning_ShowIfTrue();
await base.SaveAndCloseAsync(); await base.SaveAndCloseAsync();
} }
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync(); => await SaveAndCloseAsync();
@ -44,7 +48,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName); Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
} }
public async void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
var newTemplate = await editTemplate(Templates.Folder, settingsDisp.DownloadDecryptSettings.FolderTemplate); var newTemplate = await editTemplate(Templates.Folder, settingsDisp.DownloadDecryptSettings.FolderTemplate);
@ -52,7 +55,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
settingsDisp.DownloadDecryptSettings.FolderTemplate = newTemplate; settingsDisp.DownloadDecryptSettings.FolderTemplate = newTemplate;
} }
public async void EditFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void EditFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
var newTemplate = await editTemplate(Templates.File, settingsDisp.DownloadDecryptSettings.FileTemplate); var newTemplate = await editTemplate(Templates.File, settingsDisp.DownloadDecryptSettings.FileTemplate);
@ -60,7 +62,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
settingsDisp.DownloadDecryptSettings.FileTemplate = newTemplate; settingsDisp.DownloadDecryptSettings.FileTemplate = newTemplate;
} }
public async void EditChapterFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void EditChapterFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
var newTemplate = await editTemplate(Templates.ChapterFile, settingsDisp.DownloadDecryptSettings.ChapterFileTemplate); var newTemplate = await editTemplate(Templates.ChapterFile, settingsDisp.DownloadDecryptSettings.ChapterFileTemplate);
@ -68,7 +69,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
settingsDisp.DownloadDecryptSettings.ChapterFileTemplate = newTemplate; settingsDisp.DownloadDecryptSettings.ChapterFileTemplate = newTemplate;
} }
public async void EditCharReplacementButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void EditCharReplacementButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
var form = new LibationWinForms.Dialogs.EditReplacementChars(config); var form = new LibationWinForms.Dialogs.EditReplacementChars(config);
@ -83,7 +83,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
settingsDisp.AudioSettings.ChapterTitleTemplate = newTemplate; settingsDisp.AudioSettings.ChapterTitleTemplate = newTemplate;
} }
private async Task<string> editTemplate(Templates template, string existingTemplate) private async Task<string> editTemplate(Templates template, string existingTemplate)
{ {
var form = new EditTemplateDialog(template, existingTemplate); var form = new EditTemplateDialog(template, existingTemplate);
@ -96,7 +95,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
internal interface ISettingsDisplay internal interface ISettingsDisplay
{ {
void LoadSettings(Configuration config); void LoadSettings(Configuration config);
void SaveSettings(Configuration config); bool SaveSettings(Configuration config);
} }
public class SettingsPages : ISettingsDisplay public class SettingsPages : ISettingsDisplay
@ -119,19 +118,19 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
AudioSettings = new(config); AudioSettings = new(config);
} }
public void SaveSettings(Configuration config) public bool SaveSettings(Configuration config)
{ {
ImportantSettings.SaveSettings(config); var result = ImportantSettings.SaveSettings(config);
ImportSettings.SaveSettings(config); result &= ImportSettings.SaveSettings(config);
DownloadDecryptSettings.SaveSettings(config); result &= DownloadDecryptSettings.SaveSettings(config);
AudioSettings.SaveSettings(config); result &= AudioSettings.SaveSettings(config);
return result;
} }
} }
public class ImportantSettings : ISettingsDisplay public class ImportantSettings : ISettingsDisplay
{ {
private static Func<string, string> desc { get; } = Configuration.GetDescription;
public ImportantSettings(Configuration config) public ImportantSettings(Configuration config)
{ {
LoadSettings(config); LoadSettings(config);
@ -145,12 +144,27 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
BetaOptIn = config.BetaOptIn; BetaOptIn = config.BetaOptIn;
} }
public void SaveSettings(Configuration config) public bool SaveSettings(Configuration config)
{ {
#region validation
if (string.IsNullOrWhiteSpace(BooksDirectory))
{
MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
#endregion
LongPath lonNewBooks = BooksDirectory;
if (!System.IO.Directory.Exists(lonNewBooks))
System.IO.Directory.CreateDirectory(lonNewBooks);
config.Books = BooksDirectory; config.Books = BooksDirectory;
config.SavePodcastsToParentFolder = SavePodcastsToParentFolder; config.SavePodcastsToParentFolder = SavePodcastsToParentFolder;
config.LogLevel = LoggingLevel; config.LogLevel = LoggingLevel;
config.BetaOptIn = BetaOptIn; config.BetaOptIn = BetaOptIn;
return true;
} }
@ -161,10 +175,10 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
Configuration.KnownDirectories.MyDocs Configuration.KnownDirectories.MyDocs
}; };
public string BooksText => desc(nameof(Configuration.Books)); public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
public string SavePodcastsToParentFolderText => desc(nameof(Configuration.SavePodcastsToParentFolder)); public string SavePodcastsToParentFolderText { get; } = Configuration.GetDescription(nameof(Configuration.SavePodcastsToParentFolder));
public Serilog.Events.LogEventLevel[] LoggingLevels { get; } = Enum.GetValues<Serilog.Events.LogEventLevel>(); public Serilog.Events.LogEventLevel[] LoggingLevels { get; } = Enum.GetValues<Serilog.Events.LogEventLevel>();
public string BetaOptInText => desc(nameof(Configuration.BetaOptIn)); public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn));
public string BooksDirectory { get; set; } public string BooksDirectory { get; set; }
public bool SavePodcastsToParentFolder { get; set; } public bool SavePodcastsToParentFolder { get; set; }
@ -172,11 +186,8 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
public bool BetaOptIn { get; set; } public bool BetaOptIn { get; set; }
} }
public class ImportSettings : ISettingsDisplay public class ImportSettings : ISettingsDisplay
{ {
private static Func<string, string> desc { get; } = Configuration.GetDescription;
public ImportSettings(Configuration config) public ImportSettings(Configuration config)
{ {
LoadSettings(config); LoadSettings(config);
@ -191,20 +202,21 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
AutoDownloadEpisodes = config.AutoDownloadEpisodes; AutoDownloadEpisodes = config.AutoDownloadEpisodes;
} }
public void SaveSettings(Configuration config) public bool SaveSettings(Configuration config)
{ {
config.AutoScan = AutoScan; config.AutoScan = AutoScan;
config.ShowImportedStats = ShowImportedStats; config.ShowImportedStats = ShowImportedStats;
config.ImportEpisodes = ImportEpisodes; config.ImportEpisodes = ImportEpisodes;
config.DownloadEpisodes = DownloadEpisodes; config.DownloadEpisodes = DownloadEpisodes;
config.AutoDownloadEpisodes = AutoDownloadEpisodes; config.AutoDownloadEpisodes = AutoDownloadEpisodes;
return true;
} }
public string AutoScanText => desc(nameof(Configuration.AutoScan)); public string AutoScanText { get; } = Configuration.GetDescription(nameof(Configuration.AutoScan));
public string ShowImportedStatsText => desc(nameof(Configuration.ShowImportedStats)); public string ShowImportedStatsText { get; } = Configuration.GetDescription(nameof(Configuration.ShowImportedStats));
public string ImportEpisodesText => desc(nameof(Configuration.ImportEpisodes)); public string ImportEpisodesText { get; } = Configuration.GetDescription(nameof(Configuration.ImportEpisodes));
public string DownloadEpisodesText => desc(nameof(Configuration.DownloadEpisodes)); public string DownloadEpisodesText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadEpisodes));
public string AutoDownloadEpisodesText => desc(nameof(Configuration.AutoDownloadEpisodes)); public string AutoDownloadEpisodesText { get; } = Configuration.GetDescription(nameof(Configuration.AutoDownloadEpisodes));
public bool AutoScan { get; set; } public bool AutoScan { get; set; }
public bool ShowImportedStats { get; set; } public bool ShowImportedStats { get; set; }
@ -215,7 +227,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
public class DownloadDecryptSettings : ViewModels.ViewModelBase, ISettingsDisplay public class DownloadDecryptSettings : ViewModels.ViewModelBase, ISettingsDisplay
{ {
private static Func<string, string> desc { get; } = Configuration.GetDescription;
private bool _badBookAsk; private bool _badBookAsk;
private bool _badBookAbort; private bool _badBookAbort;
@ -246,8 +257,28 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
: Configuration.GetKnownDirectory(config.InProgress); : Configuration.GetKnownDirectory(config.InProgress);
} }
public void SaveSettings(Configuration config) public bool SaveSettings(Configuration config)
{ {
static void validationError(string text, string caption)
=> MessageBox.Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
// these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning
if (!Templates.Folder.IsValid(FolderTemplate))
{
validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
return false;
}
if (!Templates.File.IsValid(FileTemplate))
{
validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
return false;
}
if (!Templates.ChapterFile.IsValid(ChapterFileTemplate))
{
validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
return false;
}
config.BadBook config.BadBook
= BadBookAbort ? Configuration.BadBookAction.Abort = BadBookAbort ? Configuration.BadBookAction.Abort
: BadBookRetry ? Configuration.BadBookAction.Retry : BadBookRetry ? Configuration.BadBookAction.Retry
@ -260,24 +291,25 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
config.InProgress config.InProgress
= InProgressDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute = InProgressDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
: Configuration.GetKnownDirectoryPath(InProgressDirectory); : Configuration.GetKnownDirectoryPath(InProgressDirectory);
return true;
} }
public string BadBookGroupboxText => desc(nameof(Configuration.BadBook)); public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription(); public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription(); public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription(); public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription();
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription(); public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription();
public string FolderTemplateText => desc(nameof(Configuration.FolderTemplate)); public string FolderTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
public string FileTemplateText => desc(nameof(Configuration.FileTemplate)); public string FileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate));
public string ChapterFileTemplateText => desc(nameof(Configuration.ChapterFileTemplate)); public string ChapterFileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
public string EditCharReplacementText => desc(nameof(Configuration.ReplacementCharacters)); public string EditCharReplacementText { get; } = Configuration.GetDescription(nameof(Configuration.ReplacementCharacters));
public string InProgressDescriptionText => desc(nameof(Configuration.InProgress)); public string InProgressDescriptionText { get; } = Configuration.GetDescription(nameof(Configuration.InProgress));
public string FolderTemplate { get => _folderTemplate; set { this.RaiseAndSetIfChanged(ref _folderTemplate, value); } } public string FolderTemplate { get => _folderTemplate; set { this.RaiseAndSetIfChanged(ref _folderTemplate, value); } }
public string FileTemplate { get => _fileTemplate; set { this.RaiseAndSetIfChanged(ref _fileTemplate, value); } } public string FileTemplate { get => _fileTemplate; set { this.RaiseAndSetIfChanged(ref _fileTemplate, value); } }
public string ChapterFileTemplate { get => _chapterFileTemplate; set { this.RaiseAndSetIfChanged(ref _chapterFileTemplate, value); } } public string ChapterFileTemplate { get => _chapterFileTemplate; set { this.RaiseAndSetIfChanged(ref _chapterFileTemplate, value); } }
public bool BadBookAsk public bool BadBookAsk
{ {
get => _badBookAsk; get => _badBookAsk;
@ -346,7 +378,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
private int _lameBitrate; private int _lameBitrate;
private int _lameVBRQuality; private int _lameVBRQuality;
private string _chapterTitleTemplate; private string _chapterTitleTemplate;
private static Func<string, string> desc { get; } = Configuration.GetDescription;
public AudioSettings(Configuration config) public AudioSettings(Configuration config)
{ {
LoadSettings(config); LoadSettings(config);
@ -371,7 +403,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
LameVBRQuality = config.LameVBRQuality; LameVBRQuality = config.LameVBRQuality;
} }
public void SaveSettings(Configuration config) public bool SaveSettings(Configuration config)
{ {
config.CreateCueSheet = CreateCueSheet; config.CreateCueSheet = CreateCueSheet;
config.AllowLibationFixup = AllowLibationFixup; config.AllowLibationFixup = AllowLibationFixup;
@ -389,17 +421,19 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
config.LameMatchSourceBR = LameMatchSource; config.LameMatchSourceBR = LameMatchSource;
config.LameBitrate = LameBitrate; config.LameBitrate = LameBitrate;
config.LameVBRQuality = LameVBRQuality; config.LameVBRQuality = LameVBRQuality;
return true;
} }
public string CreateCueSheetText => desc(nameof(Configuration.CreateCueSheet)); public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
public string AllowLibationFixupText => desc(nameof(Configuration.AllowLibationFixup)); public string AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup));
public string DownloadCoverArtText => desc(nameof(Configuration.DownloadCoverArt)); public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt));
public string RetainAaxFileText => desc(nameof(Configuration.RetainAaxFile)); public string RetainAaxFileText { get; } = Configuration.GetDescription(nameof(Configuration.RetainAaxFile));
public string SplitFilesByChapterText => desc(nameof(Configuration.SplitFilesByChapter)); public string SplitFilesByChapterText { get; } = Configuration.GetDescription(nameof(Configuration.SplitFilesByChapter));
public string MergeOpeningEndCreditsText => desc(nameof(Configuration.MergeOpeningAndEndCredits)); public string MergeOpeningEndCreditsText { get; } = Configuration.GetDescription(nameof(Configuration.MergeOpeningAndEndCredits));
public string StripAudibleBrandingText => desc(nameof(Configuration.StripAudibleBrandAudio)); public string StripAudibleBrandingText { get; } = Configuration.GetDescription(nameof(Configuration.StripAudibleBrandAudio));
public string StripUnabridgedText => desc(nameof(Configuration.StripUnabridged)); public string StripUnabridgedText { get; } = Configuration.GetDescription(nameof(Configuration.StripUnabridged));
public string ChapterTitleTemplateText => desc(nameof(Configuration.ChapterTitleTemplate)); public string ChapterTitleTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
public bool CreateCueSheet { get; set; } public bool CreateCueSheet { get; set; }
public bool DownloadCoverArt { get; set; } public bool DownloadCoverArt { get; set; }

View File

@ -9,7 +9,7 @@ namespace LibationWinForms.AvaloniaUI.Views
{ {
private void Configure_Export() { } private void Configure_Export() { }
public async void exportLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void exportLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
try try
{ {
@ -37,11 +37,11 @@ namespace LibationWinForms.AvaloniaUI.Views
break; break;
} }
await MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName); MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
} }
catch (Exception ex) catch (Exception ex)
{ {
await MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex); MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
} }
} }
} }

View File

@ -40,7 +40,7 @@ namespace LibationWinForms.AvaloniaUI.Views
} }
catch (Exception ex) catch (Exception ex)
{ {
await MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
// re-apply last good filter // re-apply last good filter
await performFilter(lastGoodFilter); await performFilter(lastGoodFilter);

View File

@ -40,7 +40,7 @@ namespace LibationWinForms.AvaloniaUI.Views
public async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) public async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{ {
var result = await MessageBox.Show( var result = MessageBox.Show(
"This converts all m4b titles in your library to mp3 files. Original files are not deleted." "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\nFor large libraries this will take a long time and will take up more disk space."
+ "\r\n\r\nContinue?" + "\r\n\r\nContinue?"

View File

@ -15,7 +15,7 @@ namespace LibationWinForms.AvaloniaUI.Views
SetQueueCollapseState(collapseState); SetQueueCollapseState(collapseState);
} }
public async void ProductsDisplay_LiberateClicked(object sender, LibraryBook libraryBook) public void ProductsDisplay_LiberateClicked(object sender, LibraryBook libraryBook)
{ {
try try
{ {
@ -38,7 +38,7 @@ namespace LibationWinForms.AvaloniaUI.Views
if (!Go.To.File(filePath?.ShortPathName)) if (!Go.To.File(filePath?.ShortPathName))
{ {
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}"; var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
await MessageBox.Show($"File not found" + suffix); MessageBox.Show($"File not found" + suffix);
} }
} }
} }

View File

@ -35,7 +35,7 @@ namespace LibationWinForms.AvaloniaUI.Views
// in autoScan, new books SHALL NOT show dialog // in autoScan, new books SHALL NOT show dialog
try try
{ {
await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts); await LibraryCommands.ImportAccountAsync(Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -27,7 +27,7 @@ namespace LibationWinForms.AvaloniaUI.Views
public async void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
await MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account"); MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
await new Dialogs.AccountsDialog().ShowDialog(this); await new Dialogs.AccountsDialog().ShowDialog(this);
} }
@ -63,15 +63,15 @@ namespace LibationWinForms.AvaloniaUI.Views
{ {
try try
{ {
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts); var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts);
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop // 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) if (Configuration.Instance.ShowImportedStats && newAdded > 0)
await MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}"); MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
} }
catch (Exception ex) catch (Exception ex)
{ {
await MessageBox.ShowAdminAlert( MessageBox.ShowAdminAlert(
this, this,
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator", "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
"Error importing library", "Error importing library",

View File

@ -14,6 +14,6 @@ namespace LibationWinForms.AvaloniaUI.Views
public async void basicSettingsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => await new Dialogs.SettingsDialog().ShowDialog(this); public async void basicSettingsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => await new Dialogs.SettingsDialog().ShowDialog(this);
public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}"); => MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
} }
} }

View File

@ -48,7 +48,7 @@ namespace LibationWinForms.AvaloniaUI.Views
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries(); var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog( var confirmationResult = MessageBox.ShowConfirmationDialog(
this, this,
visibleLibraryBooks, visibleLibraryBooks,
"Are you sure you want to replace tags in {0}?", "Are you sure you want to replace tags in {0}?",
@ -71,7 +71,7 @@ namespace LibationWinForms.AvaloniaUI.Views
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries(); var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog( var confirmationResult = MessageBox.ShowConfirmationDialog(
this, this,
visibleLibraryBooks, visibleLibraryBooks,
"Are you sure you want to replace downloaded status in {0}?", "Are you sure you want to replace downloaded status in {0}?",
@ -89,7 +89,7 @@ namespace LibationWinForms.AvaloniaUI.Views
{ {
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries(); var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog( var confirmationResult = MessageBox.ShowConfirmationDialog(
this, this,
visibleLibraryBooks, visibleLibraryBooks,
"Are you sure you want to remove {0} from Libation's library?", "Are you sure you want to remove {0} from Libation's library?",

View File

@ -67,8 +67,7 @@ namespace LibationWinForms.AvaloniaUI.Views
private async void MainWindow_Opened(object sender, EventArgs e) private async void MainWindow_Opened(object sender, EventArgs e)
{ {
//var dialog = new EditReplacementChars();
//await dialog.ShowDialog(this);
} }
public void ProductsDisplay_Initialized1(object sender, EventArgs e) public void ProductsDisplay_Initialized1(object sender, EventArgs e)