Skip to content

Commit d10a636

Browse files
authored
Show release notes in a dialog before updating UniGetUI (#5697)
1 parent b0cf6ab commit d10a636

22 files changed

Lines changed: 972 additions & 130 deletions

‎src/Languages/lang_en.json‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2341,6 +2341,17 @@
23412341
"Manage the Python environment at {0}": "Manage the Python environment at {0}",
23422342
"Unrecognized Python": "Unrecognized Python",
23432343
"The environment variable {0} was updated": "The environment variable {0} was updated",
2344+
"UniGetUI {0} is available": "UniGetUI {0} is available",
2345+
"Download the installer in my browser": "Download the installer in my browser",
2346+
"UniGetUI will update when you quit it": "UniGetUI will update when you quit it",
2347+
"Update when I quit UniGetUI": "Update when I quit UniGetUI",
2348+
"More update options": "More update options",
2349+
"See what's new and choose when to update": "See what's new and choose when to update",
2350+
"What's new": "What's new",
2351+
"Preparing UniGetUI {0}": "Preparing UniGetUI {0}",
2352+
"Nothing will be installed until you choose to update": "Nothing will be installed until you choose to update",
2353+
"It will be installed when UniGetUI is idle in the background": "It will be installed when UniGetUI is idle in the background",
2354+
"You have {0} → {1}": "You have {0} → {1}",
23442355
"Agent Skills": "Agent Skills",
23452356
"Skills and plugins for your AI tools": "Skills and plugins for your AI tools",
23462357
"{0} could not be changed": "{0} could not be changed",

‎src/Shared/AutoUpdater.Helpers.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,32 @@ internal static bool VersionsMatch(string? a, string? b)
236236
return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase);
237237
}
238238

239+
internal enum SelfUpdateConsent
240+
{
241+
Waiting,
242+
Install,
243+
Decline,
244+
Skip,
245+
}
246+
247+
internal static SelfUpdateConsent ResolveSelfUpdateConsent(
248+
bool installRequested,
249+
bool installOnQuitRequested,
250+
bool appIsQuitting,
251+
bool skipRequested)
252+
{
253+
if (skipRequested)
254+
return SelfUpdateConsent.Skip;
255+
if (installRequested)
256+
return SelfUpdateConsent.Install;
257+
if (appIsQuitting)
258+
return installOnQuitRequested ? SelfUpdateConsent.Install : SelfUpdateConsent.Decline;
259+
return SelfUpdateConsent.Waiting;
260+
}
261+
262+
internal static bool IsSkippedVersion(string? skippedVersion, string candidateVersion) =>
263+
!string.IsNullOrWhiteSpace(skippedVersion) && VersionsMatch(skippedVersion, candidateVersion);
264+
239265
internal static bool IsPreReleaseVersion(string? rawVersion) =>
240266
TryParseUpdaterVersion(rawVersion, out SemanticVersion parsed) && parsed.IsPreRelease;
241267

‎src/UniGetUI.Avalonia/App.axaml.cs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using UniGetUI.Avalonia.Views.DialogPages;
2121
using UniGetUI.Core.Data;
2222
using UniGetUI.Core.Logging;
23+
using UniGetUI.Interface.Enums;
2324
using UniGetUI.PackageEngine;
2425
using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings;
2526

@@ -315,9 +316,23 @@ private static async Task StartupAsync(MainWindow mainWindow, string[] args)
315316
return;
316317
}
317318

319+
RouteSelfUpdateToastAction(args);
318320
await StartupArgumentProcessor.ProcessAsync(args);
319321
}
320322

323+
private static void RouteSelfUpdateToastAction(string[] args)
324+
{
325+
foreach (string arg in args)
326+
{
327+
string? action = WindowsAppNotificationBridge.TryParseToastLaunchArgument(arg);
328+
if (string.Equals(action, NotificationArguments.ShowSelfUpdate, StringComparison.OrdinalIgnoreCase))
329+
{
330+
WindowsAppNotificationBridge.RaiseActivation(action);
331+
return;
332+
}
333+
}
334+
}
335+
321336
private static void HandleSecondaryInstanceArgs(MainWindow mainWindow, string[] args)
322337
{
323338
bool isDaemonLaunch = args.Contains(AvaloniaCliHandler.DAEMON);

‎src/UniGetUI.Avalonia/Infrastructure/AvaloniaAutoUpdater.cs‎

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,19 @@ internal static partial class AvaloniaAutoUpdater
7373
/// Fired on the UI thread when a validated installer is ready. Argument is the
7474
/// human-readable version string, e.g. "4.2.1".
7575
/// </summary>
76-
public static event Action<string>? UpdateAvailable;
76+
public static event Action<string, bool>? UpdateAvailable;
77+
78+
public static event Action? PendingUpdateChanged;
79+
80+
public sealed record PendingSelfUpdate(string VersionName, string InstallerDownloadUrl, long InstallerSizeBytes);
81+
82+
public static PendingSelfUpdate? PendingUpdate { get; private set; }
83+
84+
private static void SetPendingUpdate(PendingSelfUpdate? pending)
85+
{
86+
PendingUpdate = pending;
87+
Dispatcher.UIThread.Post(() => PendingUpdateChanged?.Invoke());
88+
}
7789

7890
/// <summary>
7991
/// Fired on the UI thread to surface progress/result of an update check or
@@ -367,15 +379,16 @@ private static void OpenUpdateLog()
367379

368380
private static readonly TimeSpan IdleSampleInterval = TimeSpan.FromSeconds(5);
369381
private static volatile bool _installRequested;
382+
private static volatile bool _installOnQuitRequested;
383+
private static volatile bool _skipRequested;
370384
private static readonly HashSet<string> _backgroundInstallTriedVersions = [];
371385
private static readonly string[] UniGetUIProcessNames = ["UniGetUI", "uniget", "UniGetUI.Avalonia", "WingetUI"];
372386
private static string? _pendingInstallerPath;
373387

374388
/// <summary>
375389
/// Set to <c>true</c> when the main window is closing (user quit or hidden path).
376-
/// Mirrors WinUI's <c>AutoUpdater.ReleaseLockForAutoupdate_Window</c> — once set,
377-
/// a pending installer is allowed to launch even if the user has not yet clicked
378-
/// the banner (e.g. user quits via tray while an update is ready).
390+
/// Once set, a pending installer launches only if the user asked to update when
391+
/// UniGetUI quits; otherwise the pending update is dropped without installing.
379392
/// </summary>
380393
public static bool ReleaseLockForAutoupdate_Window;
381394

@@ -393,6 +406,25 @@ public static void TriggerInstall()
393406
LogUpdateInfo("Auto-updater: TriggerInstall invoked (user clicked Update now).");
394407
_installRequested = true;
395408
}
409+
410+
public static bool IsInstallOnQuitRequested => _installOnQuitRequested;
411+
412+
public static void RequestInstallOnQuit()
413+
{
414+
LogUpdateInfo("Auto-updater: the user asked to install the update when UniGetUI quits.");
415+
_installOnQuitRequested = true;
416+
Dispatcher.UIThread.Post(() => PendingUpdateChanged?.Invoke());
417+
}
418+
419+
public static void SkipVersion(string versionName)
420+
{
421+
LogUpdateInfo($"Auto-updater: the user skipped version {versionName}.");
422+
Settings.SetValue(Settings.K.SkippedUniGetUIVersion, versionName);
423+
_installOnQuitRequested = false;
424+
_skipRequested = true;
425+
DismissUpdateBanner();
426+
SetPendingUpdate(null);
427+
}
396428
public static async Task UpdateCheckLoopAsync()
397429
{
398430
if (Settings.Get(Settings.K.DisableAutoUpdateWingetUI))
@@ -462,7 +494,7 @@ internal static Task<bool> CheckAndInstallUpdatesAsync(bool autoLaunch = false,
462494
onJoinedActiveAttempt: () =>
463495
LogUpdateInfo("Auto-updater: an update attempt is already in progress; reusing active attempt."),
464496
onManualJoinWhileAwaitingConsent: versionName =>
465-
Dispatcher.UIThread.Post(() => UpdateAvailable?.Invoke(versionName)),
497+
Dispatcher.UIThread.Post(() => UpdateAvailable?.Invoke(versionName, true)),
466498
onManualJoinWhileBusy: () =>
467499
RaiseStatus(
468500
CoreTools.Translate("We are checking for updates."),
@@ -553,6 +585,15 @@ bool IsChannelObsolete() =>
553585
RecordTargetVersion(candidate.VersionName);
554586
LogUpdateInfo($"Update to UniGetUI {candidate.VersionName} is available.");
555587

588+
if (!IsManual() && AutoUpdaterHelpers.IsSkippedVersion(Settings.GetValue(Settings.K.SkippedUniGetUIVersion), candidate.VersionName))
589+
{
590+
LogUpdateInfo($"Auto-updater: version {candidate.VersionName} was skipped by the user, not offering it.");
591+
if (OperatingSystem.IsWindows())
592+
WindowsAppNotificationBridge.RemoveSelfUpdateAvailableNotification();
593+
MarkAttemptFinished("version skipped by the user");
594+
return true;
595+
}
596+
556597
if (OperatingSystem.IsMacOS() && ReportMacUpdateTargetIfBlocked(ResolveMacUpdateTarget()))
557598
{
558599
return false;
@@ -584,7 +625,7 @@ bool IsChannelObsolete() =>
584625
message => LogUpdateWarn(message)
585626
);
586627
LogUpdateInfo("Cached valid installer found, preparing to launch...");
587-
return await PrepareAndLaunchAsync(attempt, installerPath, candidate.InstallerHash, candidate.VersionName, autoLaunch, IsManual());
628+
return await PrepareAndLaunchAsync(attempt, installerPath, candidate.InstallerHash, candidate.VersionName, candidate.InstallerDownloadUrl, autoLaunch, IsManual());
588629
}
589630

590631
string installerValidationFailureMarkerPath =
@@ -619,9 +660,11 @@ bool IsChannelObsolete() =>
619660

620661
RaiseStatus(
621662
CoreTools.Translate(
622-
"UniGetUI version {0} is being downloaded.",
663+
"Preparing UniGetUI {0}",
623664
candidate.VersionName.ToString(CultureInfo.InvariantCulture)),
624-
CoreTools.Translate("This may take a minute or two"),
665+
Settings.Get(Settings.K.InstallUniGetUIUpdatesWhenIdle)
666+
? CoreTools.Translate("It will be installed when UniGetUI is idle in the background")
667+
: CoreTools.Translate("Nothing will be installed until you choose to update"),
625668
InfoBarSeverity.Informational,
626669
isClosable: false);
627670

@@ -664,7 +707,7 @@ await CheckInstallerHashAsync(downloadedInstallerPath, candidate.InstallerHash,
664707
message => LogUpdateWarn(message)
665708
);
666709
LogUpdateInfo("Downloaded installer is valid, preparing to launch...");
667-
return await PrepareAndLaunchAsync(attempt, installerPath, candidate.InstallerHash, candidate.VersionName, autoLaunch, IsManual());
710+
return await PrepareAndLaunchAsync(attempt, installerPath, candidate.InstallerHash, candidate.VersionName, candidate.InstallerDownloadUrl, autoLaunch, IsManual());
668711
}
669712

670713
UpdaterDownloadEngine.RecordFailure(
@@ -759,6 +802,7 @@ private static async Task<bool> PrepareAndLaunchAsync(
759802
string installerPath,
760803
string installerHash,
761804
string versionName,
805+
string installerDownloadUrl,
762806
bool autoLaunch,
763807
bool manualCheck)
764808
{
@@ -775,10 +819,13 @@ bool IsChannelObsolete() =>
775819

776820
_pendingInstallerPath = installerPath;
777821
_installRequested = false;
822+
_installOnQuitRequested = false;
823+
_skipRequested = false;
778824
ReleaseLockForAutoupdate_Notification = false;
825+
SetPendingUpdate(new PendingSelfUpdate(versionName, installerDownloadUrl, GetFileSize(installerPath)));
779826

780827
// Notify UI (update banner + toast)
781-
Dispatcher.UIThread.Post(() => UpdateAvailable?.Invoke(versionName));
828+
Dispatcher.UIThread.Post(() => UpdateAvailable?.Invoke(versionName, manualCheck));
782829
if (OperatingSystem.IsWindows())
783830
WindowsAppNotificationBridge.ShowSelfUpdateAvailableNotification(versionName);
784831
else if (OperatingSystem.IsMacOS())
@@ -817,8 +864,28 @@ bool IsChannelObsolete() =>
817864
int idleChecks = 0;
818865
DateTime nextIdleSample = DateTime.UtcNow + IdleSampleInterval;
819866

820-
while (!_installRequested && !ReleaseLockForAutoupdate_Window && !ReleaseLockForAutoupdate_Notification)
867+
while (true)
821868
{
869+
AutoUpdaterHelpers.SelfUpdateConsent consent = AutoUpdaterHelpers.ResolveSelfUpdateConsent(
870+
_installRequested || ReleaseLockForAutoupdate_Notification,
871+
_installOnQuitRequested,
872+
ReleaseLockForAutoupdate_Window,
873+
_skipRequested);
874+
if (consent is AutoUpdaterHelpers.SelfUpdateConsent.Install)
875+
break;
876+
if (consent is AutoUpdaterHelpers.SelfUpdateConsent.Skip)
877+
{
878+
MarkAttemptFinished("version skipped by the user");
879+
return true;
880+
}
881+
if (consent is AutoUpdaterHelpers.SelfUpdateConsent.Decline)
882+
{
883+
LogUpdateInfo("Auto-updater: UniGetUI is quitting and the user did not ask to update — not installing.");
884+
DismissUpdateBanner();
885+
MarkAttemptFinished("not installed - the user did not ask to update before quitting");
886+
return true;
887+
}
888+
822889
if (IsChannelObsolete())
823890
{
824891
LogUpdateInfo("Auto-updater: update channel changed while waiting for user — aborting.");
@@ -907,6 +974,20 @@ bool IsChannelObsolete() =>
907974
if (installingInBackground)
908975
AbstractOperation.HoldNewOperations = false;
909976
InstallerHandoffs.Complete(handoff);
977+
SetPendingUpdate(null);
978+
}
979+
}
980+
981+
private static long GetFileSize(string path)
982+
{
983+
try
984+
{
985+
return new FileInfo(path).Length;
986+
}
987+
catch (Exception ex)
988+
{
989+
LogUpdateWarn($"Could not read the size of {path}: {ex.Message}");
990+
return 0;
910991
}
911992
}
912993

‎src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ public static void ShowSelfUpdateAvailableNotification(string newVersion)
160160
DeliverNotification(
161161
CoreTools.Translate("{0} can be updated to version {1}", "UniGetUI", newVersion),
162162
CoreTools.Translate("You have currently version {0} installed", CoreData.VersionName),
163-
MainWindow.RuntimeNotificationLevel.Success);
163+
MainWindow.RuntimeNotificationLevel.Success,
164+
allowInAppFallback: false);
164165
}
165166
catch (Exception ex)
166167
{
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using UniGetUI.Core.Data;
2+
3+
namespace UniGetUI.Avalonia.Infrastructure;
4+
5+
internal static class ReleaseNotesLinks
6+
{
7+
public static Uri PageUri(bool darkTheme) =>
8+
new($"{CoreData.ReleaseNotesUrl}?theme={Theme(darkTheme)}");
9+
10+
public static Uri UpdateDialogUri(bool darkTheme) =>
11+
new($"{CoreData.ReleaseNotesUrl}?theme={Theme(darkTheme)}&utm_source=unigetui&utm_medium=app&utm_campaign=self-update");
12+
13+
private static string Theme(bool darkTheme) => darkTheme ? "dark-neutral" : "light";
14+
15+
public static bool IsReleaseNotesPage(Uri? uri)
16+
{
17+
if (uri is null || !uri.IsAbsoluteUri)
18+
return false;
19+
20+
Uri page = new(CoreData.ReleaseNotesUrl);
21+
return string.Equals(uri.Scheme, page.Scheme, StringComparison.OrdinalIgnoreCase)
22+
&& string.Equals(uri.Host, page.Host, StringComparison.OrdinalIgnoreCase)
23+
&& string.Equals(uri.AbsolutePath, page.AbsolutePath, StringComparison.OrdinalIgnoreCase);
24+
}
25+
26+
public static string BrowserDownloadUrl(string versionName, string installerDownloadUrl, bool isWindows) =>
27+
isWindows && !string.IsNullOrWhiteSpace(installerDownloadUrl)
28+
? installerDownloadUrl
29+
: CoreData.GetGitHubReleasePageUrlFromTag(CoreData.GetGitHubReleaseTag(ShortVersion(versionName)));
30+
31+
public static string ShortVersion(string versionName)
32+
{
33+
string trimmed = versionName.Trim();
34+
return Version.TryParse(trimmed, out Version? parsed) && parsed.Revision == 0
35+
? $"{parsed.Major}.{parsed.Minor}.{parsed.Build}"
36+
: trimmed;
37+
}
38+
39+
public static bool CanOpenInBrowser(Uri? uri) =>
40+
uri is { IsAbsoluteUri: true }
41+
&& (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp);
42+
}

‎src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ public static void ShowSelfUpdateAvailableNotification(string newVersion)
233233
CoreTools.Translate("{0} can be updated to version {1}", "UniGetUI", newVersion),
234234
CoreTools.Translate("You have currently version {0} installed", CoreData.VersionName),
235235
MainWindow.RuntimeNotificationLevel.Success,
236-
launchAction: NotificationArguments.Show,
236+
launchAction: NotificationArguments.ShowSelfUpdate,
237+
allowInAppFallback: false,
237238
tag: SelfUpdateToastTag);
238239
}
239240
catch (Exception ex)

0 commit comments

Comments
 (0)