From c2f9392fa849db60325e1ceafa1754811cb363c1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 21:09:23 +0000 Subject: [PATCH 1/3] Let users turn off the startup update check Libation asked GitHub for a newer release every time it started, with no way to stop it. That is noise for anyone whose install is updated by something else - a package manager, or an AppImage updater - because the prompt it raises is one they can do nothing useful with. Add CheckForUpgradesAtStartup, on by default so nothing changes for people who rely on the prompt. Only the automatic check is optional: the About window's "Check for Upgrade" button and the CLI's `version --check` ask for a check outright, so they run either way. That is why the setting is read in a new CheckForUpgradeAtStartupAsync rather than inside CheckForUpgradeAsync, which the startup path and the About button share. The new setting takes the slot of BetaOptIn, which is deleted here. It was declared, described and logged, but no axaml or designer ever bound it and nothing read the value: GetLatestRelease only ever asks for the stable release, so there was no beta channel for it to select. A stale BetaOptIn key in an existing Settings.json needs no migration, since PersistentDictionary ignores keys with no matching property. Closes #1999 Co-authored-by: rmcrackan --- Source/AppScaffolding/LibationScaffolding.cs | 3 +- .../Controls/Settings/Important.axaml | 35 +++++--- .../Settings/ImportantSettingsVM.cs | 6 +- .../Views/MainWindow.axaml.cs | 2 +- .../Configuration.HelpText.cs | 11 +++ .../Configuration.PersistentSettings.cs | 4 +- Source/LibationUiBase/Upgrader.cs | 17 ++++ .../Dialogs/SettingsDialog.Designer.cs | 13 +++ .../Dialogs/SettingsDialog.Important.cs | 4 + Source/LibationWinForms/Form1.Upgrade.cs | 2 +- .../UpgradeCheckSettingTests.cs | 52 ++++++++++++ .../StartupUpgradeCheckTests.cs | 81 +++++++++++++++++++ docs/advanced/advanced.md | 2 + docs/installation/linux.md | 4 +- 14 files changed, 216 insertions(+), 20 deletions(-) create mode 100644 Source/_Tests/LibationFileManager.Tests/UpgradeCheckSettingTests.cs create mode 100644 Source/_Tests/LibationUiBase.Tests/StartupUpgradeCheckTests.cs diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 9c953ca0..fed07a93 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -332,7 +332,8 @@ public static class LibationScaffolding config.ImportEpisodes, config.ImportPlusTitles, config.DownloadEpisodes, - config.BetaOptIn, + // Off means no startup upgrade prompt, which is otherwise indistinguishable from a broken check + config.CheckForUpgradesAtStartup, config.UseCoverAsFolderIcon, config.LibationFiles, AudibleFileStorage.BooksDirectory, diff --git a/Source/LibationAvalonia/Controls/Settings/Important.axaml b/Source/LibationAvalonia/Controls/Settings/Important.axaml index 8a67612e..49f5efe2 100644 --- a/Source/LibationAvalonia/Controls/Settings/Important.axaml +++ b/Source/LibationAvalonia/Controls/Settings/Important.axaml @@ -69,21 +69,30 @@ - - - - - - + Spacing="5"> + + + + + + + + + + + v.Value == config.CreationTime) ?? DateTimeSources[0]; LastWriteTime = DateTimeSources.SingleOrDefault(v => v.Value == config.LastWriteTime) ?? DateTimeSources[0]; UseWebView = config.UseWebView; + CheckForUpgradesAtStartup = config.CheckForUpgradesAtStartup; LoggingLevel = config.LogLevel; GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor); GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor); @@ -77,6 +78,7 @@ public class ImportantSettingsVM : ViewModelBase config.CreationTime = CreationTime.Value; config.LastWriteTime = LastWriteTime.Value; config.UseWebView = UseWebView; + config.CheckForUpgradesAtStartup = CheckForUpgradesAtStartup; config.LogLevel = LoggingLevel; config.TokenStorageMethod = SelectedTokenStorageMethod; initialTokenStorageMethod = SelectedTokenStorageMethod; @@ -145,10 +147,11 @@ public class ImportantSettingsVM : ViewModelBase /// When true, the Use WebView setting is disabled (e.g. when running in Linux Snap to avoid portal/sandbox crashes). public bool UseWebViewSettingDisabled => Configuration.IsRunningUnderSnap; public string UseWebViewSnapMessage { get; } = Configuration.IsRunningUnderSnap ? "Disabled when running in Linux Snap (avoids login crash). Use external browser instead." : ""; + public string CheckForUpgradesAtStartupText { get; } = Configuration.GetDescription(nameof(Configuration.CheckForUpgradesAtStartup)); + public string CheckForUpgradesAtStartupTip { get; } = Configuration.GetHelpText(nameof(Configuration.CheckForUpgradesAtStartup)); public Serilog.Events.LogEventLevel[] LoggingLevels { get; } = Enum.GetValues(); public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor)); public string GridFontScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridFontScaleFactor)); - public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn)); public EnumDisplay[] Themes { get; } = Enum.GetValues() .Select(v => new EnumDisplay(v)) @@ -171,6 +174,7 @@ public class ImportantSettingsVM : ViewModelBase public EnumDisplay CreationTime { get; set; } public EnumDisplay LastWriteTime { get; set; } public bool UseWebView { get; set; } + public bool CheckForUpgradesAtStartup { get; set; } public Serilog.Events.LogEventLevel LoggingLevel { get; set; } public bool EncryptTokens diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs index d7e12e06..4a6e4e1c 100644 --- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs +++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs @@ -278,7 +278,7 @@ public partial class MainWindow : ReactiveWindow upgrader.UpgradeFailed += async (_, message) => await Dispatcher.UIThread.InvokeAsync(() => { setProgressVisible(false); MessageBox.Show(this, message, "Upgrade Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); }); #if !DEBUG - Opened += async (_, _) => await upgrader.CheckForUpgradeAsync(upgradeAvailable); + Opened += async (_, _) => await upgrader.CheckForUpgradeAtStartupAsync(upgradeAvailable); #endif } diff --git a/Source/LibationFileManager/Configuration.HelpText.cs b/Source/LibationFileManager/Configuration.HelpText.cs index 863d4bd6..e281bd34 100644 --- a/Source/LibationFileManager/Configuration.HelpText.cs +++ b/Source/LibationFileManager/Configuration.HelpText.cs @@ -168,6 +168,17 @@ public partial class Configuration When enabled, books from the Audible Plus catalog (titles you stream or borrow under your membership, not purchased) are imported into Libation. Downloading or liberating many Plus titles in a short time can cause Audible to temporarily deny content licenses ("license denied") for a day or two. That limit is enforced by Audible, not Libation — waiting and retrying usually fixes it. If problems persist after several days, report on Libation's GitHub with logs. + """ }, + {nameof(CheckForUpgradesAtStartup), """ + When enabled, Libation asks GitHub whether a newer + release exists each time it starts, and offers it to + you if there is one. + + Turn this off if something else keeps Libation up to + date, such as a package manager or an AppImage + updater. You can still check whenever you like: + Help > About has a "Check for Upgrade" button that + works either way. """ } }.AsReadOnly(); diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs index ebb48f9b..cab2bd1c 100644 --- a/Source/LibationFileManager/Configuration.PersistentSettings.cs +++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs @@ -117,8 +117,8 @@ public partial class Configuration [Description("Book display font size")] public float GridFontScaleFactor { get => float.Min(2, float.Max(0.5f, GetNonString(defaultValue: 1f))); set => SetNonString(value); } - [Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")] - public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); } + [Description("Check for new Libation versions at startup")] + public bool CheckForUpgradesAtStartup { get => GetNonString(defaultValue: true); set => SetNonString(value); } [Description("Location for book storage. Includes destination of newly liberated books")] public LongPath? Books diff --git a/Source/LibationUiBase/Upgrader.cs b/Source/LibationUiBase/Upgrader.cs index 20f2380a..04cf8378 100644 --- a/Source/LibationUiBase/Upgrader.cs +++ b/Source/LibationUiBase/Upgrader.cs @@ -226,6 +226,23 @@ public abstract class UpgraderBase ? new(false, ApplicationControlUpgradeMessage, ApplicationControlUpgradeSummary) : new(platformCanUpgrade, null, null); + /// + /// The check both GUIs run when their main window opens, skipped when the user has turned off + /// . Only this automatic check is optional: + /// the About window's "Check for Upgrade" button asks for a check outright and calls + /// regardless of the setting. + /// + public async Task CheckForUpgradeAtStartupAsync(Func upgradeAvailableHandler) + { + if (!Configuration.Instance.CheckForUpgradesAtStartup) + { + Serilog.Log.Logger.Information("Skipping the startup upgrade check: {Setting} is off.", nameof(Configuration.CheckForUpgradesAtStartup)); + return; + } + + await CheckForUpgradeAsync(upgradeAvailableHandler); + } + /// Check for upgrade and invoke if an update is available. Returns the check outcome so the UI can show "up to date", "update available", or "unable to determine". public async Task CheckForUpgradeAsync(Func upgradeAvailableHandler) { diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs index a63f1095..619026a2 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs @@ -69,6 +69,7 @@ absFolderLbl = new System.Windows.Forms.Label(); absFolderCb = new System.Windows.Forms.ComboBox(); tab1ImportantSettings = new System.Windows.Forms.TabPage(); + checkForUpgradesCbox = new System.Windows.Forms.CheckBox(); themeLbl = new System.Windows.Forms.Label(); themeCb = new System.Windows.Forms.ComboBox(); label22 = new System.Windows.Forms.Label(); @@ -405,6 +406,16 @@ loggingLevelCb.Size = new System.Drawing.Size(129, 23); loggingLevelCb.TabIndex = 4; // + // checkForUpgradesCbox + // + checkForUpgradesCbox.AutoSize = true; + checkForUpgradesCbox.Location = new System.Drawing.Point(6, 569); + checkForUpgradesCbox.Name = "checkForUpgradesCbox"; + checkForUpgradesCbox.Size = new System.Drawing.Size(300, 19); + checkForUpgradesCbox.TabIndex = 13; + checkForUpgradesCbox.Text = "[Check for upgrades at startup]"; + checkForUpgradesCbox.UseVisualStyleBackColor = true; + // // tabControl // tabControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; @@ -423,6 +434,7 @@ // tab1ImportantSettings.AutoScroll = true; tab1ImportantSettings.BackColor = System.Drawing.SystemColors.Window; + tab1ImportantSettings.Controls.Add(checkForUpgradesCbox); tab1ImportantSettings.Controls.Add(themeLbl); tab1ImportantSettings.Controls.Add(themeCb); tab1ImportantSettings.Controls.Add(label22); @@ -1926,6 +1938,7 @@ private System.Windows.Forms.CheckBox createCueSheetCbox; private System.Windows.Forms.CheckBox autoScanCb; private System.Windows.Forms.CheckBox useWebViewCb; + private System.Windows.Forms.CheckBox checkForUpgradesCbox; private System.Windows.Forms.CheckBox downloadCoverArtCbox; private System.Windows.Forms.CheckBox autoDownloadEpisodesCb; private System.Windows.Forms.CheckBox saveEpisodesToSeriesFolderCbox; diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs index 78457031..d52df52e 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs @@ -42,6 +42,8 @@ public partial class SettingsDialog lastWriteTimeLbl.Text = desc(nameof(config.LastWriteTime)); gridScaleFactorLbl.Text = desc(nameof(config.GridScaleFactor)); gridFontScaleFactorLbl.Text = desc(nameof(config.GridFontScaleFactor)); + checkForUpgradesCbox.Text = desc(nameof(config.CheckForUpgradesAtStartup)); + toolTip.SetToolTip(checkForUpgradesCbox, Configuration.GetHelpText(nameof(config.CheckForUpgradesAtStartup))); var dateTimeSources = Enum.GetValues().Select(v => new EnumDisplay(v)).ToArray(); creationTimeCb.Items.AddRange(dateTimeSources); @@ -70,6 +72,7 @@ public partial class SettingsDialog saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder; overwriteExistingCbox.Checked = config.OverwriteExisting; + checkForUpgradesCbox.Checked = config.CheckForUpgradesAtStartup; gridScaleFactorTbar.Value = scaleFactorToLinearRange(config.GridScaleFactor); gridFontScaleFactorTbar.Value = scaleFactorToLinearRange(config.GridFontScaleFactor); @@ -195,6 +198,7 @@ public partial class SettingsDialog config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked; config.OverwriteExisting = overwriteExistingCbox.Checked; + config.CheckForUpgradesAtStartup = checkForUpgradesCbox.Checked; config.CreationTime = (creationTimeCb.SelectedItem as EnumDisplay)?.Value ?? Configuration.DateTimeSource.File; config.LastWriteTime = (lastWriteTimeCb.SelectedItem as EnumDisplay)?.Value ?? Configuration.DateTimeSource.File; diff --git a/Source/LibationWinForms/Form1.Upgrade.cs b/Source/LibationWinForms/Form1.Upgrade.cs index d06d59b8..1b63f8da 100644 --- a/Source/LibationWinForms/Form1.Upgrade.cs +++ b/Source/LibationWinForms/Form1.Upgrade.cs @@ -27,7 +27,7 @@ public partial class Form1 upgrader.UpgradeFailed += (_, message) => Invoke(() => { setProgressVisible(false); MessageBox.Show(this, message, "Upgrade Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); }); #if !DEBUG - Shown += async (_, _) => await upgrader.CheckForUpgradeAsync(upgradeAvailable); + Shown += async (_, _) => await upgrader.CheckForUpgradeAtStartupAsync(upgradeAvailable); #endif } diff --git a/Source/_Tests/LibationFileManager.Tests/UpgradeCheckSettingTests.cs b/Source/_Tests/LibationFileManager.Tests/UpgradeCheckSettingTests.cs new file mode 100644 index 00000000..9f7feedf --- /dev/null +++ b/Source/_Tests/LibationFileManager.Tests/UpgradeCheckSettingTests.cs @@ -0,0 +1,52 @@ +using AssertionHelper; +using LibationFileManager; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UpgradeCheckSettingTests; + +[TestClass] +[DoNotParallelize] +public class UpgradeCheckSettingTests +{ + [TestCleanup] + public void Cleanup() + { + Configuration.RestoreSingletonInstance(); + } + + // The update check is opt-out, so every install that predates the setting has to keep checking + // without anything having written the key. + [TestMethod] + public void Default_when_missing_is_enabled() + { + var config = Configuration.CreateMockInstance(); + + config.Exists(nameof(Configuration.CheckForUpgradesAtStartup)).Should().BeFalse(); + Assert.IsTrue(config.CheckForUpgradesAtStartup); + } + + [TestMethod] + public void Round_trips_off_and_on() + { + var config = Configuration.CreateMockInstance(); + + config.CheckForUpgradesAtStartup = false; + Assert.IsFalse(config.CheckForUpgradesAtStartup); + Assert.IsFalse(config.CreateEphemeralCopy().CheckForUpgradesAtStartup); + + config.CheckForUpgradesAtStartup = true; + Assert.IsTrue(config.CheckForUpgradesAtStartup); + Assert.IsTrue(config.CreateEphemeralCopy().CheckForUpgradesAtStartup); + } + + // get-setting and the -o override both reflect over Configuration properties carrying + // [Description], so a missing attribute would silently drop the setting from the CLI. + [TestMethod] + public void Has_a_description_so_the_cli_can_see_it() + { + var description = Configuration.GetDescription(nameof(Configuration.CheckForUpgradesAtStartup)); + + Assert.AreNotEqual($"[{nameof(Configuration.CheckForUpgradesAtStartup)}]", description); + StringAssert.Contains(description, "startup"); + } +} diff --git a/Source/_Tests/LibationUiBase.Tests/StartupUpgradeCheckTests.cs b/Source/_Tests/LibationUiBase.Tests/StartupUpgradeCheckTests.cs new file mode 100644 index 00000000..e7504944 --- /dev/null +++ b/Source/_Tests/LibationUiBase.Tests/StartupUpgradeCheckTests.cs @@ -0,0 +1,81 @@ +using LibationFileManager; + +namespace LibationUiBase.Tests; + +/// +/// Covers the one thing the CheckForUpgradesAtStartup setting has to do: keep the startup check off +/// the network when it is off, and leave it alone when it is on. +/// +/// is the seam. With +/// false, the check reports its own distinctive failure and the flow returns before reaching +/// InteropFactory, whose CanUpgrade throws off-platform. That failure message is +/// therefore proof that the check ran, and its absence proof that it did not. +/// +/// +[TestClass] +[DoNotParallelize] +public class StartupUpgradeCheckTests +{ + private const string CheckRanMessage = "Mock Check For Upgrade Failed"; + + [TestCleanup] + public void Cleanup() + { + Configuration.RestoreSingletonInstance(); + } + + [TestMethod] + public async Task Startup_checks_when_the_setting_is_on() + { + var config = Configuration.CreateMockInstance(); + config.CheckForUpgradesAtStartup = true; + + var (upgrader, failures) = BuildUpgraderThatFailsItsCheck(); + await upgrader.CheckForUpgradeAtStartupAsync(ShouldNotBeCalled); + + Assert.AreEqual(1, failures.Count); + StringAssert.Contains(failures[0], CheckRanMessage); + } + + [TestMethod] + public async Task Startup_does_not_check_when_the_setting_is_off() + { + var config = Configuration.CreateMockInstance(); + config.CheckForUpgradesAtStartup = false; + + var (upgrader, failures) = BuildUpgraderThatFailsItsCheck(); + await upgrader.CheckForUpgradeAtStartupAsync(ShouldNotBeCalled); + + Assert.AreEqual(0, failures.Count); + } + + // Turning the automatic check off must not disable the About window's "Check for Upgrade" + // button, which calls CheckForUpgradeAsync directly. + [TestMethod] + public async Task A_requested_check_still_runs_when_the_setting_is_off() + { + var config = Configuration.CreateMockInstance(); + config.CheckForUpgradesAtStartup = false; + + var (upgrader, failures) = BuildUpgraderThatFailsItsCheck(); + var result = await upgrader.CheckForUpgradeAsync(ShouldNotBeCalled); + + Assert.AreEqual(AppScaffolding.VersionCheckOutcome.UnableToDetermine, result.Outcome); + Assert.AreEqual(1, failures.Count); + StringAssert.Contains(failures[0], CheckRanMessage); + } + + private static (MockUpgrader Upgrader, List Failures) BuildUpgraderThatFailsItsCheck() + { + var upgrader = new MockUpgrader { CheckForUpgradeSucceeds = false }; + List failures = []; + upgrader.UpgradeFailed += (_, message) => failures.Add(message); + return (upgrader, failures); + } + + private static Task ShouldNotBeCalled(UpgradeEventArgs e) + { + Assert.Fail("The upgrade-available handler ran, but the check was supposed to fail before an update could be offered."); + return Task.CompletedTask; + } +} diff --git a/docs/advanced/advanced.md b/docs/advanced/advanced.md index 03f07678..5df69700 100644 --- a/docs/advanced/advanced.md +++ b/docs/advanced/advanced.md @@ -12,6 +12,8 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi ## Settings +- Check for new Libation versions at startup. Enabled by default: each time Libation starts it asks GitHub whether a newer release exists, and offers it to you if there is one. Turn it off if something else keeps Libation up to date, such as a package manager or an AppImage updater. Turning it off only stops the automatic check - Help > About still has a "Check for Upgrade" button that works either way. + - Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all. In addition to the options that are enabled if you allow Libation to "fix up" the audiobook, it does the following: diff --git a/docs/installation/linux.md b/docs/installation/linux.md index 5a97c53d..b1cb6b46 100644 --- a/docs/installation/linux.md +++ b/docs/installation/linux.md @@ -62,7 +62,9 @@ sudo dnf5 install ./libation.rpm am -i libation ``` Thanks to Package Forge dev [Samuel](https://github.com/Samueru-sama) for [AppImage](https://github.com/pkgforge-dev/Libation-AppImage) maintenence. - + +When your package manager updates Libation for you, the startup update check has nothing useful to tell you. Turn off "Check for new Libation versions at startup" on the Important settings tab to stop it. Help > About still has a "Check for Upgrade" button whenever you want to look. + ### Arch Linux ```bash From e4dde1e6c850a6b458ecc2e295a149fecb2c0ec8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 21:47:31 +0000 Subject: [PATCH 2/3] Point the update-check copy at Settings > About, not Help > About Both Chardonnay and Classic list About... under the Settings menu; neither has a Help menu. Co-authored-by: rmcrackan --- Source/LibationFileManager/Configuration.HelpText.cs | 4 ++-- docs/advanced/advanced.md | 2 +- docs/installation/linux.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/LibationFileManager/Configuration.HelpText.cs b/Source/LibationFileManager/Configuration.HelpText.cs index e281bd34..9de8dac4 100644 --- a/Source/LibationFileManager/Configuration.HelpText.cs +++ b/Source/LibationFileManager/Configuration.HelpText.cs @@ -177,8 +177,8 @@ public partial class Configuration Turn this off if something else keeps Libation up to date, such as a package manager or an AppImage updater. You can still check whenever you like: - Help > About has a "Check for Upgrade" button that - works either way. + Settings > About has a "Check for Upgrade" button + that works either way. """ } }.AsReadOnly(); diff --git a/docs/advanced/advanced.md b/docs/advanced/advanced.md index 5df69700..6eb622f1 100644 --- a/docs/advanced/advanced.md +++ b/docs/advanced/advanced.md @@ -12,7 +12,7 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi ## Settings -- Check for new Libation versions at startup. Enabled by default: each time Libation starts it asks GitHub whether a newer release exists, and offers it to you if there is one. Turn it off if something else keeps Libation up to date, such as a package manager or an AppImage updater. Turning it off only stops the automatic check - Help > About still has a "Check for Upgrade" button that works either way. +- Check for new Libation versions at startup. Enabled by default: each time Libation starts it asks GitHub whether a newer release exists, and offers it to you if there is one. Turn it off if something else keeps Libation up to date, such as a package manager or an AppImage updater. Turning it off only stops the automatic check - Settings > About still has a "Check for Upgrade" button that works either way. - Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all. diff --git a/docs/installation/linux.md b/docs/installation/linux.md index b1cb6b46..1e162839 100644 --- a/docs/installation/linux.md +++ b/docs/installation/linux.md @@ -63,7 +63,7 @@ sudo dnf5 install ./libation.rpm ``` Thanks to Package Forge dev [Samuel](https://github.com/Samueru-sama) for [AppImage](https://github.com/pkgforge-dev/Libation-AppImage) maintenence. -When your package manager updates Libation for you, the startup update check has nothing useful to tell you. Turn off "Check for new Libation versions at startup" on the Important settings tab to stop it. Help > About still has a "Check for Upgrade" button whenever you want to look. +When your package manager updates Libation for you, the startup update check has nothing useful to tell you. Turn off "Check for new Libation versions at startup" on the Important settings tab to stop it. Settings > About still has a "Check for Upgrade" button whenever you want to look. ### Arch Linux From 612ec05b19f36e68550ac58d407f585ad79f22cd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 22:05:59 +0000 Subject: [PATCH 3/3] Stop a download-only upgrade notice from starting an install When Libation cannot install an upgrade itself - a portable or AppImage install, a Linux build with no package-manager symlink, macOS outside /Applications - the prompt is a notice with a download link, and Chardonnay relabels its button from "Yes" to "OK" to say so. The button still closed with DialogResult.OK, which MainWindow and the About window both read as "yes, install it", so acknowledging the notice downloaded the release and ran the auto-upgrader anyway. That could only fail, and it failed loudly: an "Upgrade Failed" error box in answer to dismissing a notice. The button now closes with Cancel when there was no upgrade on offer, so the answer matches the question. Classic already did the equivalent, hiding its Yes button and relabelling No. UpgraderBase gets the matching guard. It already refused to install under Windows Application Control on the grounds that a UI ignoring CapUpgrade must not be able to start an upgrade that leaves Libation unable to start; that reasoning covers every capped upgrade, not just the Windows one, so the stop is now keyed on CapUpgrade itself via MayInstallUpgrade. Co-authored-by: rmcrackan --- .../UpgradeNotificationDialog.axaml.cs | 8 ++++++- Source/LibationUiBase/Upgrader.cs | 22 ++++++++++++++----- .../UpgradeCapabilityTests.cs | 20 +++++++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs index 27da46a5..2f1efacd 100644 --- a/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs @@ -14,6 +14,7 @@ public partial class UpgradeNotificationDialog : DialogWindow public string? ReleaseNotes { get; } public string? OkText { get; } private string? PackageUrl { get; } + private bool CanUpgrade { get; } = true; public UpgradeNotificationDialog() { if (Design.IsDesignMode) @@ -33,6 +34,7 @@ public partial class UpgradeNotificationDialog : DialogWindow public UpgradeNotificationDialog(UpgradeProperties upgradeProperties, bool canUpgrade, string? upgradeUnavailableReason = null) : this() { Title = $"Libation version {upgradeProperties.LatestRelease.ToVersionString()} is now available."; + CanUpgrade = canUpgrade; PackageUrl = upgradeProperties.ZipUrl; DownloadLinkText = upgradeProperties.ZipName; ReleaseNotes = upgradeProperties.Notes; @@ -41,7 +43,11 @@ public partial class UpgradeNotificationDialog : DialogWindow DataContext = this; } - public void OK_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => Close(DialogResult.OK); + // When Libation cannot install the upgrade itself, this button reads "OK" and the dialog is a + // notice with a download link. Reporting OK there would be read as "yes, install it", starting a + // download and an install that was never on offer, so acknowledging a notice closes and no more. + public void OK_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + => Close(CanUpgrade ? DialogResult.OK : DialogResult.Cancel); public void DontRemind_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => Close(DialogResult.Ignore); public void Download_Tapped(object sender, Avalonia.Input.TappedEventArgs e) => Go.To.Url(PackageUrl); diff --git a/Source/LibationUiBase/Upgrader.cs b/Source/LibationUiBase/Upgrader.cs index 04cf8378..b204a8bc 100644 --- a/Source/LibationUiBase/Upgrader.cs +++ b/Source/LibationUiBase/Upgrader.cs @@ -226,6 +226,15 @@ public abstract class UpgraderBase ? new(false, ApplicationControlUpgradeMessage, ApplicationControlUpgradeSummary) : new(platformCanUpgrade, null, null); + /// + /// Whether the flow may go on to download and install. The dialog's answer is not enough on its + /// own: when Libation cannot install the upgrade itself, the prompt is a notice with a download + /// link, so a UI that reports acceptance anyway must not be able to start an install that was + /// never on offer - or, under Application Control, one that leaves Libation unable to start. + /// + internal static bool MayInstallUpgrade(bool userAccepted, bool capUpgrade) + => userAccepted && capUpgrade; + /// /// The check both GUIs run when their main window opens, skipped when the user has turned off /// . Only this automatic check is optional: @@ -283,13 +292,14 @@ public abstract class UpgraderBase if (upgradeEventArgs.Ignore) config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpgrade); - if (!upgradeEventArgs.InstallUpgrade) return result; - - // A second stop, because a UI that ignores CapUpgrade must not be able to start an - // upgrade that ends with Libation unable to start. - if (applicationControlBlocksUpgrade) + if (!MayInstallUpgrade(upgradeEventArgs.InstallUpgrade, capability.CapUpgrade)) { - Serilog.Log.Logger.Information("Skipped the in-app upgrade to {LatestRelease}: Windows Application Control is enforcing.", upgradeProperties.LatestRelease); + if (upgradeEventArgs.InstallUpgrade) + Serilog.Log.Logger.Information( + "Skipped the in-app upgrade to {LatestRelease}: {Reason}.", + upgradeProperties.LatestRelease, + applicationControlBlocksUpgrade ? "Windows Application Control is enforcing" : "this install cannot upgrade itself"); + return result; } diff --git a/Source/_Tests/LibationUiBase.Tests/UpgradeCapabilityTests.cs b/Source/_Tests/LibationUiBase.Tests/UpgradeCapabilityTests.cs index 70a5b727..13ee83cd 100644 --- a/Source/_Tests/LibationUiBase.Tests/UpgradeCapabilityTests.cs +++ b/Source/_Tests/LibationUiBase.Tests/UpgradeCapabilityTests.cs @@ -51,4 +51,24 @@ public class UpgradeCapabilityTests Assert.IsNull(capability.Reason); Assert.IsNull(capability.Summary); } + + [TestMethod] + public void An_accepted_upgrade_installs_when_the_platform_can() + => Assert.IsTrue(UpgraderBase.MayInstallUpgrade(userAccepted: true, capUpgrade: true)); + + // Chardonnay's prompt relabels its button to "OK" when Libation cannot install the upgrade + // itself, but still reported acceptance, so acknowledging a download-link notice began a + // download and an install that was never on offer and could only fail. + [TestMethod] + public void Acceptance_is_not_enough_when_libation_cannot_install_the_upgrade() + => Assert.IsFalse(UpgraderBase.MayInstallUpgrade(userAccepted: true, capUpgrade: false)); + + // InstallUpgrade defaults to true, so a UI that never answers must not be taken as a yes on a + // platform that cannot install. + [TestMethod] + public void Declining_never_installs() + { + Assert.IsFalse(UpgraderBase.MayInstallUpgrade(userAccepted: false, capUpgrade: true)); + Assert.IsFalse(UpgraderBase.MayInstallUpgrade(userAccepted: false, capUpgrade: false)); + } }