diff --git a/Source/LibationAvalonia/Walkthrough.cs b/Source/LibationAvalonia/Walkthrough.cs index 305bac0b..e8d0e2a6 100644 --- a/Source/LibationAvalonia/Walkthrough.cs +++ b/Source/LibationAvalonia/Walkthrough.cs @@ -1,4 +1,3 @@ -using AppScaffolding; using ApplicationServices; using AudibleUtilities; using Avalonia.Controls; @@ -11,6 +10,7 @@ using Dinah.Core.StepRunner; using LibationAvalonia.Dialogs; using LibationAvalonia.Views; using LibationFileManager; +using LibationUiBase; using LibationUiBase.Forms; using System.Collections.Generic; using System.Linq; @@ -20,14 +20,7 @@ namespace LibationAvalonia; internal class Walkthrough { - private readonly Dictionary settingTabMessages = new() - { - { "Important Settings", "From here you can change where liberated books are stored and how detailed Libation's logs are.\r\n\r\nIf you experience a problem and need help, you'll be asked to provide your log file. In certain circumstances we may need you to reproduce the error with a higher level of logging detail.\r\n\r\nFor best use with screen readers, uncheck \"Use Libation's built-in web browser to log into Audible?\"."}, - { "Import Library", "In this tab you can change how your library is scanned and imported into Libation, as well as automatic liberation."}, - { "Download/Decrypt", "These settings allow you to control how liberated files and folders are named and stored.\r\nYou can customize the 'Naming Templates' to use any number of the audiobook's properties to build a customized file and folder naming format. Learn more about the syntax from the wiki at\r\n\r\n" + LibationScaffolding.NamingTemplatesDocUrl}, - { "Audio File Settings", "Control how audio files are decrypted, including audio format and metadata handling.\r\n\r\nIf you choose to split your audiobook into multiple files by chapter marker, you may edit the chapter file 'Naming Template' to control how each chapter file is named."}, - }; - + private readonly HashSet shownSettingTabs = []; private static readonly IBrush FlashColor = Brushes.DodgerBlue; private readonly MainWindow MainForm; private readonly AsyncStepSequence sequence = new(); @@ -54,7 +47,8 @@ internal class Walkthrough private async Task ShowAccountDialog() { - if (!await ProceedMessageBox("First, add your Audible account(s).", "Add Accounts")) + var proceed = WalkthroughMessages.AddAccountsProceed; + if (!await ProceedMessageBox(proceed.Message, proceed.Title)) return false; await Task.Delay(750); @@ -62,14 +56,16 @@ internal class Walkthrough await displayControlAsync(MainForm.accountsToolStripMenuItem); var accountSettings = new AccountsDialog(); - accountSettings.Opened += async (_, _) => await MessageBox.Show(accountSettings, "Add your Audible account(s), then save.\r\n\r\nMost accounts use email and a normal region. If yours is a pre-Amazon username login, choose a pre-amazon locale and enter that username.", "Add an Account"); + var onDialog = WalkthroughMessages.AddAccountOnDialog; + accountSettings.Opened += async (_, _) => await MessageBox.Show(accountSettings, onDialog.Message, onDialog.Title); await accountSettings.ShowDialog(MainForm); return true; } private async Task ShowSettingsDialog() { - if (!await ProceedMessageBox("Next, adjust Libation's settings", "Change Settings")) + var proceed = WalkthroughMessages.ChangeSettingsProceed; + if (!await ProceedMessageBox(proceed.Message, proceed.Title)) return false; await Task.Delay(750); @@ -97,14 +93,19 @@ internal class Walkthrough return; tabsToVisit.Remove(selectedTab); - if (!selectedTab.IsVisible || !(selectedTab.Header is TextBlock header && header.Text is string text && settingTabMessages.ContainsKey(text))) return; + if (!selectedTab.IsVisible + || selectedTab.Header is not TextBlock header + || header.Text is not string text + || !WalkthroughMessages.TryGetSettingsTab(text, out var message)) + return; if (tabsToVisit.Count == 0) settingsDialog.saveBtn.Content = "Save"; - await MessageBox.Show(settingsDialog, settingTabMessages[header.Text], header.Text + " Tab", MessageBoxButtons.OK); + if (!shownSettingTabs.Add(text)) + return; - settingTabMessages.Remove(header.Text); + await MessageBox.Show(settingsDialog, message.Message, message.Title, MessageBoxButtons.OK); } async void SettingsDialog_Opened(object? sender, System.EventArgs e) @@ -138,13 +139,13 @@ internal class Walkthrough if (count < 1) { - await MessageBox.Show(MainForm, "Add an Audible account, then sync your library through the 'Import' menu", "Add an Audible Account", MessageBoxButtons.OK, MessageBoxIcon.Information); + var noAccounts = WalkthroughMessages.NoAccountsYet; + await MessageBox.Show(MainForm, noAccounts.Message, noAccounts.Title, MessageBoxButtons.OK, MessageBoxIcon.Information); return false; } - var accounts = count > 1 ? "accounts" : "account"; - var library = count > 1 ? "libraries" : "library"; - if (!await ProceedMessageBox($"Finally, scan your Audible {accounts} to sync your {library} with Libation.\r\n\r\nIf this is your first time scanning an account, you'll be prompted to enter your account's password to log into your Audible account.", $"Scan {accounts}")) + var proceed = WalkthroughMessages.ScanProceed(count); + if (!await ProceedMessageBox(proceed.Message, proceed.Title)) return false; var scanItem = count > 1 ? MainForm.scanLibraryOfAllAccountsToolStripMenuItem : MainForm.scanLibraryToolStripMenuItem; @@ -184,7 +185,8 @@ internal class Walkthrough var firstAuthor = getFirstAuthor()?.SurroundWithQuotes(); if (firstAuthor == null) return true; - if (!await ProceedMessageBox("You can filter the grid entries by searching", "Searching")) + var proceed = WalkthroughMessages.SearchingProceed; + if (!await ProceedMessageBox(proceed.Message, proceed.Title)) return false; await displayControlAsync(MainForm.filterSearchTb); @@ -202,7 +204,8 @@ internal class Walkthrough await Task.Delay(1000); - await MessageBox.Show(MainForm, "Libation provides a built-in cheat sheet for its query language", "Search Cheat Sheet"); + var cheatSheet = WalkthroughMessages.SearchCheatSheet; + await MessageBox.Show(MainForm, cheatSheet.Message, cheatSheet.Title); await displayControlAsync(MainForm.filterHelpBtn); @@ -218,7 +221,8 @@ internal class Walkthrough var firstAuthor = getFirstAuthor()?.SurroundWithQuotes(); if (firstAuthor == null) return true; - if (!await ProceedMessageBox("Queries that you perform regularly can be added to 'Quick Filters'", "Quick Filters")) + var proceed = WalkthroughMessages.QuickFiltersProceed; + if (!await ProceedMessageBox(proceed.Message, proceed.Title)) return false; MainForm.filterSearchTb.Text = firstAuthor; @@ -232,7 +236,8 @@ internal class Walkthrough await displayControlAsync(editQuickFiltersToolStripMenuItem); var editQuickFilters = new EditQuickFilters(); - editQuickFilters.Opened += async (_, _) => await MessageBox.Show(editQuickFilters, "From here you can edit, delete, and change the order of Quick Filters", "Editing Quick Filters"); + var editMsg = WalkthroughMessages.EditQuickFilters; + editQuickFilters.Opened += async (_, _) => await MessageBox.Show(editQuickFilters, editMsg.Message, editMsg.Title); await editQuickFilters.ShowDialog(MainForm); return true; @@ -240,7 +245,8 @@ internal class Walkthrough private async Task ShowTourComplete() { - await MessageBox.Show(MainForm, "You're now ready to begin using Libation.\r\n\r\nEnjoy!", "Tour Finished"); + var finished = WalkthroughMessages.TourFinished; + await MessageBox.Show(MainForm, finished.Message, finished.Title); return true; } diff --git a/Source/LibationUiBase/WalkthroughMessages.cs b/Source/LibationUiBase/WalkthroughMessages.cs new file mode 100644 index 00000000..1f1b21d5 --- /dev/null +++ b/Source/LibationUiBase/WalkthroughMessages.cs @@ -0,0 +1,100 @@ +using AppScaffolding; +using System.Collections.Generic; + +namespace LibationUiBase; + +/// UI-agnostic title and body for a walkthrough MessageBox. +public readonly record struct WalkthroughMessage(string Title, string Message); + +/// +/// Shared guided-tour copy for WinForms and Avalonia. +/// Settings tab bodies are keyed by each UI's tab header text so screen-reader tips stay on the correct tab. +/// +public static class WalkthroughMessages +{ + private const string ScreenReaderTip + = """For best use with screen readers, uncheck "Use Libation's built-in web browser to log into Audible?"."""; + + private const string ImportantSettingsBody + = "From here you can change where liberated books are stored and how detailed Libation's logs are.\r\n\r\nIf you experience a problem and need help, you'll be asked to provide your log file. In certain circumstances we may need you to reproduce the error with a higher level of logging detail."; + + private const string ImportLibraryBody + = "In this tab you can change how your library is scanned and imported into Libation, as well as automatic liberation."; + + private static readonly string DownloadDecryptBody + = "These settings allow you to control how liberated files and folders are named and stored.\r\nYou can customize the 'Naming Templates' to use any number of the audiobook's properties to build a customized file and folder naming format. Learn more about the syntax from the wiki at\r\n\r\n" + + LibationScaffolding.NamingTemplatesDocUrl; + + private const string AudioFileBody + = "Control how audio files are decrypted, including audio format and metadata handling.\r\n\r\nIf you choose to split your audiobook into multiple files by chapter marker, you may edit the chapter file 'Naming Template' to control how each chapter file is named."; + + /// + /// Tab header text (WinForms TabPage.Text / Avalonia header) -> message body. + /// Screen-reader tip stays on Import for Classic and Important Settings for Chardonnay. + /// + private static readonly Dictionary SettingsTabBodies = new() + { + // WinForms + ["Important settings"] = ImportantSettingsBody, + ["Import library"] = ImportLibraryBody + "\r\n\r\n" + ScreenReaderTip, + ["Download/Decrypt"] = DownloadDecryptBody, + ["Audio File Options"] = AudioFileBody, + + // Avalonia + ["Important Settings"] = ImportantSettingsBody + "\r\n\r\n" + ScreenReaderTip, + ["Import Library"] = ImportLibraryBody, + ["Audio File Settings"] = AudioFileBody, + }; + + public static WalkthroughMessage AddAccountsProceed { get; } + = new("Add Accounts", "First, add your Audible account(s)."); + + public static WalkthroughMessage AddAccountOnDialog { get; } + = new( + "Add an Account", + "Add your Audible account(s), then save.\r\n\r\nMost accounts use email and a normal region. If yours is a pre-Amazon username login, choose a pre-amazon locale and enter that username."); + + public static WalkthroughMessage ChangeSettingsProceed { get; } + = new("Change Settings", "Next, adjust Libation's settings"); + + public static WalkthroughMessage NoAccountsYet { get; } + = new( + "Add an Audible Account", + "Add an Audible account, then sync your library through the 'Import' menu."); + + public static WalkthroughMessage SearchingProceed { get; } + = new("Searching", "You can filter the grid entries by searching"); + + public static WalkthroughMessage SearchCheatSheet { get; } + = new("Search Cheat Sheet", "Libation provides a built-in cheat sheet for its query language"); + + public static WalkthroughMessage QuickFiltersProceed { get; } + = new("Quick Filters", "Queries that you perform regularly can be added to 'Quick Filters'"); + + public static WalkthroughMessage EditQuickFilters { get; } + = new("Editing Quick Filters", "From here you can edit, delete, and change the order of Quick Filters"); + + public static WalkthroughMessage TourFinished { get; } + = new("Tour Finished", "You're now ready to begin using Libation.\r\n\r\nEnjoy!"); + + public static WalkthroughMessage ScanProceed(int accountCount) + { + var accounts = accountCount > 1 ? "accounts" : "account"; + var library = accountCount > 1 ? "libraries" : "library"; + return new( + $"Scan {accounts}", + $"Finally, scan your Audible {accounts} to sync your {library} with Libation.\r\n\r\nIf this is your first time scanning an account, you'll be prompted to enter your account's password to log into your Audible account."); + } + + public static bool TryGetSettingsTab(string tabHeader, out WalkthroughMessage message) + { + if (!SettingsTabBodies.TryGetValue(tabHeader, out var body)) + { + message = default; + return false; + } + + message = new(tabHeader + " Tab", body); + return true; + } +} diff --git a/Source/LibationWinForms/Walkthrough.cs b/Source/LibationWinForms/Walkthrough.cs index 8d1359aa..9c101bcc 100644 --- a/Source/LibationWinForms/Walkthrough.cs +++ b/Source/LibationWinForms/Walkthrough.cs @@ -1,9 +1,9 @@ -using AppScaffolding; using ApplicationServices; using AudibleUtilities; using Dinah.Core; using Dinah.Core.StepRunner; using LibationFileManager; +using LibationUiBase; using LibationWinForms.Dialogs; using System; using System.Collections.Generic; @@ -16,14 +16,7 @@ namespace LibationWinForms; internal class Walkthrough { - private readonly Dictionary settingTabMessages = new() - { - { "Important settings", "From here you can change where liberated books are stored and how detailed Libation's logs are.\r\n\r\nIf you experience a problem and need help, you'll be asked to provide your log file. In certain circumstances we may need you to reproduce the error with a higher level of logging detail."}, - { "Import library", "In this tab you can change how your library is scanned and imported into Libation, as well as automatic liberation.\r\n\r\nFor best use with screen readers, uncheck \"Use Libation's built-in web browser to log into Audible?\"."}, - { "Download/Decrypt", "These settings allow you to control how liberated files and folders are named and stored.\r\nYou can customize the 'Naming Templates' to use any number of the audiobook's properties to build a customized file and folder naming format. Learn more about the syntax from the wiki at\r\n\r\n" + LibationScaffolding.NamingTemplatesDocUrl}, - { "Audio File Options", "Control how audio files are decrypted, including audio format and metadata handling.\r\n\r\nIf you choose to split your audiobook into multiple files by chapter marker, you may edit the chapter file 'Naming Template' to control how each chapter file is named."}, - }; - + private readonly HashSet shownSettingTabs = []; private static readonly Color FlashColor = Color.DodgerBlue; private readonly Form1 MainForm; private readonly AsyncStepSequence sequence = new(); @@ -49,7 +42,8 @@ internal class Walkthrough private async Task ShowAccountDialog() { - if (!ProceedMessageBox("First, add your Audible account(s).", "Add Accounts")) + var proceed = WalkthroughMessages.AddAccountsProceed; + if (!ProceedMessageBox(proceed.Message, proceed.Title)) return false; await Task.Delay(750); @@ -58,14 +52,16 @@ internal class Walkthrough using var accountSettings = MainForm.Invoke(() => new AccountsDialog()); accountSettings.StartPosition = FormStartPosition.CenterParent; - accountSettings.Shown += (_, _) => MessageBox.Show(accountSettings, "Add your Audible account(s), then save.\r\n\r\nMost accounts use email and a normal region. If yours is a pre-Amazon username login, choose a pre-amazon locale and enter that username.", "Add an Account"); + var onDialog = WalkthroughMessages.AddAccountOnDialog; + accountSettings.Shown += (_, _) => MessageBox.Show(accountSettings, onDialog.Message, onDialog.Title); MainForm.Invoke(() => accountSettings.ShowDialog(MainForm)); return true; } private async Task ShowSettingsDialog() { - if (!ProceedMessageBox("Next, adjust Libation's settings", "Change Settings")) + var proceed = WalkthroughMessages.ChangeSettingsProceed; + if (!ProceedMessageBox(proceed.Message, proceed.Title)) return false; await Task.Delay(750); @@ -100,11 +96,12 @@ internal class Walkthrough settingsDialog.saveBtn.Visible = true; } - if (!selectedTab.Visible || !settingTabMessages.ContainsKey(selectedTab.Text)) return; + if (!selectedTab.Visible + || !WalkthroughMessages.TryGetSettingsTab(selectedTab.Text, out var message) + || !shownSettingTabs.Add(selectedTab.Text)) + return; - MessageBox.Show(selectedTab, settingTabMessages[selectedTab.Text], selectedTab.Text + " Tab", MessageBoxButtons.OK); - - settingTabMessages.Remove(selectedTab.Text); + MessageBox.Show(selectedTab, message.Message, message.Title, MessageBoxButtons.OK); } void SettingsDialog_FormClosing(object? sender, FormClosingEventArgs e) @@ -125,13 +122,13 @@ internal class Walkthrough if (count < 1) { - MainForm.Invoke(() => MessageBox.Show(MainForm, "Add an Audible account, then sync your library through the 'Import' menu.", "Add an Audible Account", MessageBoxButtons.OK, MessageBoxIcon.Information)); + var noAccounts = WalkthroughMessages.NoAccountsYet; + MainForm.Invoke(() => MessageBox.Show(MainForm, noAccounts.Message, noAccounts.Title, MessageBoxButtons.OK, MessageBoxIcon.Information)); return true; } - var accounts = count > 1 ? "accounts" : "account"; - var library = count > 1 ? "libraries" : "library"; - if (!ProceedMessageBox($"Finally, scan your Audible {accounts} to sync your {library} with Libation.\r\n\r\nIf this is your first time scanning an account, you'll be prompted to enter your account's password to log into your Audible account.", $"Scan {accounts}")) + var proceed = WalkthroughMessages.ScanProceed(count); + if (!ProceedMessageBox(proceed.Message, proceed.Title)) return false; var scanItem = count > 1 ? MainForm.scanLibraryOfAllAccountsToolStripMenuItem : MainForm.scanLibraryToolStripMenuItem; @@ -160,7 +157,8 @@ internal class Walkthrough var firstAuthor = getFirstAuthor()?.SurroundWithQuotes(); if (firstAuthor == null) return true; - if (!ProceedMessageBox("You can filter the grid entries by searching", "Searching")) + var proceed = WalkthroughMessages.SearchingProceed; + if (!ProceedMessageBox(proceed.Message, proceed.Title)) return false; await displayControlAsync(MainForm.filterSearchTb); @@ -178,7 +176,8 @@ internal class Walkthrough await Task.Delay(1000); - MessageBox.Show(MainForm, "Libation provides a built-in cheat sheet for its query language", "Search Cheat Sheet"); + var cheatSheet = WalkthroughMessages.SearchCheatSheet; + MessageBox.Show(MainForm, cheatSheet.Message, cheatSheet.Title); await displayControlAsync(MainForm.filterHelpBtn); @@ -194,7 +193,8 @@ internal class Walkthrough var firstAuthor = getFirstAuthor()?.SurroundWithQuotes(); if (firstAuthor == null) return true; - if (!ProceedMessageBox("Queries that you perform regularly can be added to 'Quick Filters'", "Quick Filters")) + var proceed = WalkthroughMessages.QuickFiltersProceed; + if (!ProceedMessageBox(proceed.Message, proceed.Title)) return false; MainForm.Invoke(() => MainForm.filterSearchTb.Text = firstAuthor); @@ -206,7 +206,8 @@ internal class Walkthrough await displayControlAsync(MainForm.editQuickFiltersToolStripMenuItem); var editQuickFilters = MainForm.Invoke(() => new EditQuickFilters()); - editQuickFilters.Shown += (_, _) => MessageBox.Show(editQuickFilters, "From here you can edit, delete, and change the order of Quick Filters", "Editing Quick Filters"); + var editMsg = WalkthroughMessages.EditQuickFilters; + editQuickFilters.Shown += (_, _) => MessageBox.Show(editQuickFilters, editMsg.Message, editMsg.Title); MainForm.Invoke(editQuickFilters.ShowDialog); return true; @@ -214,7 +215,8 @@ internal class Walkthrough private Task ShowTourComplete() { - MessageBox.Show(MainForm, "You're now ready to begin using Libation.\r\n\r\nEnjoy!", "Tour Finished"); + var finished = WalkthroughMessages.TourFinished; + MessageBox.Show(MainForm, finished.Message, finished.Title); return Task.FromResult(true); }