@@ -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
0 commit comments