From 5f093b06ec70d36f3ab9291f8b9b85fab2ca1f2e Mon Sep 17 00:00:00 2001 From: MBucari Date: Wed, 5 Nov 2025 22:42:15 -0700 Subject: [PATCH] Improve Chardonnay setup reliability. --- Source/LibationAvalonia/App.axaml.cs | 422 +++++++++--------- .../LibationAvalonia/Dialogs/DialogWindow.cs | 7 + .../Dialogs/MessageBoxWindow.axaml.cs | 14 +- 3 files changed, 222 insertions(+), 221 deletions(-) diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs index d5c2cb31..db969cf4 100644 --- a/Source/LibationAvalonia/App.axaml.cs +++ b/Source/LibationAvalonia/App.axaml.cs @@ -1,265 +1,261 @@ using ApplicationServices; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.Threading; +using Dinah.Core; using LibationAvalonia.Dialogs; +using LibationAvalonia.Themes; using LibationAvalonia.Views; using LibationFileManager; +using LibationUiBase.Forms; using System; using System.Collections.Generic; using System.IO; -using System.Threading.Tasks; -using Avalonia.Threading; -using Dinah.Core; -using LibationAvalonia.Themes; -using Avalonia.Data.Core.Plugins; using System.Linq; -using LibationUiBase.Forms; -using Avalonia.Controls; +using System.Threading.Tasks; #nullable enable -namespace LibationAvalonia +namespace LibationAvalonia; + +public class App : Application { - public class App : Application + public static Task>? LibraryTask { get; set; } + public static ChardonnayTheme? DefaultThemeColors { get; private set; } + public static MainWindow? MainWindow { get; private set; } + public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/"); + public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet."); + + public static Stream OpenAsset(string assetRelativePath) + => AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath)); + + public override void Initialize() => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() { - public static Task>? LibraryTask { get; set; } - public static ChardonnayTheme? DefaultThemeColors { get; private set; } - public static MainWindow? MainWindow { get; private set; } - public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/"); - public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet."); + DefaultThemeColors = ChardonnayTheme.GetLiveTheme(); - public static Stream OpenAsset(string assetRelativePath) - => AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath)); - - public override void Initialize() + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - AvaloniaXamlLoader.Load(this); - } + // Chardonnay uses the OnLastWindowClose shutdown mode. As long as the application lifetime + // has one active window, the application will stay alive. Setup windows must be daisy chained, + // each closing windows opens the next window before closing itself to prevent the app from exiting. - public override void OnFrameworkInitializationCompleted() - { - DefaultThemeColors = ChardonnayTheme.GetLiveTheme(); + MessageBoxBase.ShowAsyncImpl = (owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition) => + MessageBox.Show(owner as Window, message, caption, buttons, icon, defaultButton, saveAndRestorePosition); - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + // Avoid duplicate validations from both Avalonia and the CommunityToolkit. + // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins + DisableAvaloniaDataAnnotationValidation(); + + Configuration config = Configuration.Instance; + + if (!config.LibationSettingsAreValid) { - MessageBoxBase.ShowAsyncImpl = (owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition) => - MessageBox.Show(owner as Window, message, caption, buttons, icon, defaultButton, saveAndRestorePosition); + string defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory; - // Avoid duplicate validations from both Avalonia and the CommunityToolkit. - // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins - DisableAvaloniaDataAnnotationValidation(); + // check for existing settings in default location + string defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json"); + if (Configuration.SettingsFileIsValid(defaultSettingsFile)) + Configuration.SetLibationFiles(defaultLibationFilesDir); - var config = Configuration.Instance; - - if (!config.LibationSettingsAreValid) + if (config.LibationSettingsAreValid) { - var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory; - - // check for existing settings in default location - var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json"); - if (Configuration.SettingsFileIsValid(defaultSettingsFile)) - Configuration.SetLibationFiles(defaultLibationFilesDir); - - if (config.LibationSettingsAreValid) - { - LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); - ShowMainWindow(desktop); - } - else - { - var setupDialog = new SetupDialog { Config = config }; - setupDialog.Closing += Setup_Closing; - desktop.MainWindow = setupDialog; - } + LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); + ShowMainWindow(desktop); } else - ShowMainWindow(desktop); - } - - base.OnFrameworkInitializationCompleted(); - } - private void DisableAvaloniaDataAnnotationValidation() - { - // Get an array of plugins to remove - var dataValidationPluginsToRemove = - BindingPlugins.DataValidators.OfType().ToArray(); - - // remove each entry found - foreach (var plugin in dataValidationPluginsToRemove) - { - BindingPlugins.DataValidators.Remove(plugin); - } - } - - private async void Setup_Closing(object? sender, System.ComponentModel.CancelEventArgs e) - { - if (sender is not SetupDialog setupDialog || ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) - return; - - try - { - // all returns should be preceded by either: - // - if config.LibationSettingsAreValid - // - error message, Exit() - if (setupDialog.IsNewUser) { - Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory); - setupDialog.Config.Books = Configuration.DefaultBooksDirectory; - - if (setupDialog.Config.LibationSettingsAreValid) - { - string? theme = setupDialog.SelectedTheme.Content as string; - - setupDialog.Config.SetString(theme, nameof(ThemeVariant)); - - await RunMigrationsAsync(setupDialog.Config); - LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); - AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists(); - ShowMainWindow(desktop); - } - else - { - e.Cancel = true; - await CancelInstallation(setupDialog); - } + SetupDialog setupDialog = new() { Config = config }; + setupDialog.Closing += (_, e) => SetupClosing(setupDialog, desktop, e); + desktop.MainWindow = setupDialog; } - else if (setupDialog.IsReturningUser) + } + else + { + ShowMainWindow(desktop); + } + } + + base.OnFrameworkInitializationCompleted(); + } + + private void DisableAvaloniaDataAnnotationValidation() + { + // Get an array of plugins to remove + DataAnnotationsValidationPlugin[] dataValidationPluginsToRemove = + BindingPlugins.DataValidators.OfType().ToArray(); + + // remove each entry found + foreach (DataAnnotationsValidationPlugin? plugin in dataValidationPluginsToRemove) + { + BindingPlugins.DataValidators.Remove(plugin); + } + } + + private async void SetupClosing(SetupDialog setupDialog, IClassicDesktopStyleApplicationLifetime desktop, System.ComponentModel.CancelEventArgs e) + { + try + { + if (setupDialog.IsNewUser) + { + Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory); + setupDialog.Config.Books = Configuration.DefaultBooksDirectory; + + if (setupDialog.Config.LibationSettingsAreValid) { - ShowLibationFilesDialog(desktop, setupDialog.Config, OnLibationFilesCompleted); + string? theme = setupDialog.SelectedTheme.Content as string; + setupDialog.Config.SetString(theme, nameof(ThemeVariant)); + + await RunMigrationsAsync(setupDialog.Config); + LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); + AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists(); + ShowMainWindow(desktop); } else { e.Cancel = true; await CancelInstallation(setupDialog); - return; } - } - catch (Exception ex) + else if (setupDialog.IsReturningUser) { - var title = "Fatal error, pre-logging"; - var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file."; - try - { - await MessageBox.ShowAdminAlert(setupDialog, body, title, ex); - } - catch - { - await MessageBox.Show(setupDialog, $"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error); - } - return; - } - } - - private async Task RunMigrationsAsync(Configuration config) - { - // most migrations go in here - AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config); - - await MessageBox.VerboseLoggingWarning_ShowIfTrue(); - - // logging is init'd here - AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config); - } - - private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action OnClose) - { - var libationFilesDialog = new LibationFilesDialog(); - desktop.MainWindow = libationFilesDialog; - libationFilesDialog.Show(); - - void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e) - { - libationFilesDialog.Closing -= WindowClosing; - e.Cancel = true; - OnClose?.Invoke(desktop, libationFilesDialog, config); - } - libationFilesDialog.Closing += WindowClosing; - } - - private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config) - { - Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory); - if (config.LibationSettingsAreValid) - { - await RunMigrationsAsync(config); - - LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); - AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists(); - ShowMainWindow(desktop); + ShowLibationFilesDialog(desktop, setupDialog.Config); } else { - // path did not result in valid settings - var continueResult = await MessageBox.Show( - libationFilesDialog, - $"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}", - "New install?", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question); + e.Cancel = true; + await CancelInstallation(setupDialog); + } + } + catch (Exception ex) + { + string title = "Fatal error, pre-logging"; + string body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file."; - if (continueResult == DialogResult.Yes) + MessageBoxAlertAdminDialog alert = new(body, title, ex); + desktop.MainWindow = alert; + alert.Show(); + } + } + + private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config) + { + LibationFilesDialog libationFilesDialog = new(); + desktop.MainWindow = libationFilesDialog; + libationFilesDialog.Show(); + + async void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e) + { + libationFilesDialog.Closing -= WindowClosing; + e.Cancel = true; + if (libationFilesDialog.DialogResult == DialogResult.OK) + OnLibationFilesCompleted(desktop, libationFilesDialog, config); + else + await CancelInstallation(libationFilesDialog); + } + libationFilesDialog.Closing += WindowClosing; + } + + private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config) + { + Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory); + if (config.LibationSettingsAreValid) + { + await RunMigrationsAsync(config); + + LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); + AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists(); + ShowMainWindow(desktop); + } + else + { + // path did not result in valid settings + DialogResult continueResult = await MessageBox.Show( + libationFilesDialog, + $"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}", + "New install?", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + + if (continueResult == DialogResult.Yes) + { + config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books)); + + if (config.LibationSettingsAreValid) { - config.Books = Configuration.DefaultBooksDirectory; - - if (config.LibationSettingsAreValid) - { - await RunMigrationsAsync(config); - LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); - AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists(); - ShowMainWindow(desktop); - } - else - await CancelInstallation(libationFilesDialog); + await RunMigrationsAsync(config); + LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); + AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists(); + ShowMainWindow(desktop); } else + { await CancelInstallation(libationFilesDialog); + } } - - libationFilesDialog.Close(); - } - - static async Task CancelInstallation(Window window) - { - await MessageBox.Show(window, "Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning); - Environment.Exit(0); - } - - private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop) - { - Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged; - Current.ActualThemeVariantChanged += OnActualThemeVariantChanged; - OnActualThemeVariantChanged(Current, EventArgs.Empty); - - var mainWindow = new MainWindow(); - desktop.MainWindow = MainWindow = mainWindow; - mainWindow.Loaded += MainWindow_Loaded; - mainWindow.RestoreSizeAndLocation(Configuration.Instance); - mainWindow.Show(); - } - - [PropertyChangeFilter(nameof(ThemeVariant))] - private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e) - => OpenAndApplyTheme(e.NewValue as string); - - private static void OnActualThemeVariantChanged(object? sender, EventArgs e) - => OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant))); - - private static void OpenAndApplyTheme(string? themeVariant) - { - using var themePersister = ChardonnayThemePersister.Create(); - themePersister?.Target.ApplyTheme(themeVariant); - } - - private static async void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (LibraryTask is not null && MainWindow is not null) + else { - var library = await LibraryTask; - await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library)); + await CancelInstallation(libationFilesDialog); } } + + libationFilesDialog.Close(); + } + + private static async Task CancelInstallation(Window window) + { + await MessageBox.Show(window, "Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning); + Environment.Exit(-1); + } + + private async Task RunMigrationsAsync(Configuration config) + { + // most migrations go in here + AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config); + + await MessageBox.VerboseLoggingWarning_ShowIfTrue(); + + // logging is init'd here + AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config); + } + + private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop) + { + Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged; + Current.ActualThemeVariantChanged += OnActualThemeVariantChanged; + OnActualThemeVariantChanged(Current, EventArgs.Empty); + + MainWindow mainWindow = new(); + desktop.MainWindow = MainWindow = mainWindow; + mainWindow.Loaded += MainWindow_Loaded; + mainWindow.RestoreSizeAndLocation(Configuration.Instance); + mainWindow.Show(); + } + + [PropertyChangeFilter(nameof(ThemeVariant))] + private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e) + => OpenAndApplyTheme(e.NewValue as string); + + private static void OnActualThemeVariantChanged(object? sender, EventArgs e) + => OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant))); + + private static void OpenAndApplyTheme(string? themeVariant) + { + using ChardonnayThemePersister? themePersister = ChardonnayThemePersister.Create(); + themePersister?.Target.ApplyTheme(themeVariant); + } + + private static async void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (LibraryTask is not null && MainWindow is not null) + { + List library = await LibraryTask; + await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library)); + } } } diff --git a/Source/LibationAvalonia/Dialogs/DialogWindow.cs b/Source/LibationAvalonia/Dialogs/DialogWindow.cs index 4cd4b2b5..1bb07286 100644 --- a/Source/LibationAvalonia/Dialogs/DialogWindow.cs +++ b/Source/LibationAvalonia/Dialogs/DialogWindow.cs @@ -16,6 +16,7 @@ namespace LibationAvalonia.Dialogs public bool SaveAndRestorePosition { get; set; } public Control ControlToFocusOnShow { get; set; } protected override Type StyleKeyOverride => typeof(DialogWindow); + public DialogResult DialogResult { get; private set; } = DialogResult.None; public DialogWindow(bool saveAndRestorePosition = true) { @@ -74,6 +75,12 @@ namespace LibationAvalonia.Dialogs ControlToFocusOnShow?.Focus(); } + public void Close(DialogResult dialogResult) + { + DialogResult = dialogResult; + base.Close(dialogResult); + } + protected virtual void SaveAndClose() => Close(DialogResult.OK); protected virtual async Task SaveAndCloseAsync() => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(SaveAndClose); protected virtual void CancelAndClose() => Close(DialogResult.Cancel); diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs index 1633112f..bd77f825 100644 --- a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs @@ -19,12 +19,10 @@ namespace LibationAvalonia.Dialogs protected override void SaveAndClose() { } - public DialogResult DialogResult { get; private set; } - public void Button1_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) { var vm = DataContext as MessageBoxViewModel; - DialogResult = vm.Buttons switch + var dialogResult = vm.Buttons switch { MessageBoxButtons.OK => DialogResult.OK, MessageBoxButtons.OKCancel => DialogResult.OK, @@ -35,12 +33,12 @@ namespace LibationAvalonia.Dialogs MessageBoxButtons.CancelTryContinue => DialogResult.Cancel, _ => DialogResult.None }; - Close(DialogResult); + Close(dialogResult); } public void Button2_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) { var vm = DataContext as MessageBoxViewModel; - DialogResult = vm.Buttons switch + var dialogResult = vm.Buttons switch { MessageBoxButtons.OKCancel => DialogResult.Cancel, MessageBoxButtons.AbortRetryIgnore => DialogResult.Retry, @@ -50,19 +48,19 @@ namespace LibationAvalonia.Dialogs MessageBoxButtons.CancelTryContinue => DialogResult.TryAgain, _ => DialogResult.None }; - Close(DialogResult); + Close(dialogResult); } public void Button3_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) { var vm = DataContext as MessageBoxViewModel; - DialogResult = vm.Buttons switch + var dialogResult = vm.Buttons switch { MessageBoxButtons.AbortRetryIgnore => DialogResult.Ignore, MessageBoxButtons.YesNoCancel => DialogResult.Cancel, MessageBoxButtons.CancelTryContinue => DialogResult.Continue, _ => DialogResult.None }; - Close(DialogResult); + Close(dialogResult); } } }