Skip to content

Commit 0f2f182

Browse files
authored
Avalonia: Add the auto Update feature (#4651)
1 parent 6088358 commit 0f2f182

6 files changed

Lines changed: 245 additions & 70 deletions

File tree

src/UniGetUI.Avalonia/Infrastructure/AvaloniaAutoUpdater.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,10 @@ private static async Task<bool> PrepareAndLaunchAsync(
181181

182182
// Notify UI (update banner + toast)
183183
Dispatcher.UIThread.Post(() => UpdateAvailable?.Invoke(versionName));
184-
WindowsAppNotificationBridge.ShowSelfUpdateAvailableNotification(versionName);
184+
if (OperatingSystem.IsWindows())
185+
WindowsAppNotificationBridge.ShowSelfUpdateAvailableNotification(versionName);
186+
else if (OperatingSystem.IsMacOS())
187+
MacOsNotificationBridge.ShowSelfUpdateAvailableNotification(versionName);
185188

186189
if (autoLaunch)
187190
{

src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,10 @@ private static void ShowOperationProgressNotification(AbstractOperation op)
126126
$"{title}. {message}",
127127
AutomationLiveSetting.Polite);
128128

129-
if (WindowsAppNotificationBridge.ShowProgress(op))
129+
if (OperatingSystem.IsWindows() && WindowsAppNotificationBridge.ShowProgress(op))
130130
return;
131131

132-
if (MacOsNotificationBridge.ShowProgress(op))
132+
if (OperatingSystem.IsMacOS() && MacOsNotificationBridge.ShowProgress(op))
133133
return;
134134

135135
if (TryGetMainWindow() is not { } mainWindow)
@@ -160,10 +160,10 @@ private static void ShowOperationSuccessNotification(AbstractOperation op)
160160

161161
WindowsAppNotificationBridge.RemoveProgress(op);
162162

163-
if (WindowsAppNotificationBridge.ShowSuccess(op))
163+
if (OperatingSystem.IsWindows() && WindowsAppNotificationBridge.ShowSuccess(op))
164164
return;
165165

166-
if (MacOsNotificationBridge.ShowSuccess(op))
166+
if (OperatingSystem.IsMacOS() && MacOsNotificationBridge.ShowSuccess(op))
167167
return;
168168

169169
if (TryGetMainWindow() is not { } mainWindow)
@@ -194,10 +194,10 @@ private static void ShowOperationFailureNotification(AbstractOperation op)
194194

195195
WindowsAppNotificationBridge.RemoveProgress(op);
196196

197-
if (WindowsAppNotificationBridge.ShowError(op))
197+
if (OperatingSystem.IsWindows() && WindowsAppNotificationBridge.ShowError(op))
198198
return;
199199

200-
if (MacOsNotificationBridge.ShowError(op))
200+
if (OperatingSystem.IsMacOS() && MacOsNotificationBridge.ShowError(op))
201201
return;
202202

203203
if (TryGetMainWindow() is not { } mainWindow)

src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs

Lines changed: 50 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics;
12
using System.Runtime.InteropServices;
23
using UniGetUI.Core.Data;
34
using UniGetUI.Core.Logging;
@@ -9,37 +10,18 @@
910
namespace UniGetUI.Avalonia.Infrastructure;
1011

1112
/// <summary>
12-
/// macOS system notification delivery via NSUserNotificationCenter (ObjC runtime P/Invoke).
13-
/// Mirrors the pattern of WindowsAppNotificationBridge: guards on OS check, silent fallback on failure.
13+
/// macOS system notification delivery via osascript (works on all macOS versions).
14+
/// NSUserNotificationCenter was removed in macOS 14; UNUserNotificationCenter requires
15+
/// ObjC blocks that are impractical via pure P/Invoke. osascript is always available.
16+
/// Callers are responsible for the OperatingSystem.IsMacOS() guard before invoking.
1417
/// </summary>
1518
internal static class MacOsNotificationBridge
1619
{
17-
private static bool? _available;
18-
private static readonly object _lock = new();
19-
20-
private static bool IsAvailable()
21-
{
22-
if (!OperatingSystem.IsMacOS()) return false;
23-
lock (_lock)
24-
{
25-
if (_available.HasValue) return _available.Value;
26-
try
27-
{
28-
_available = objc_getClass("NSUserNotificationCenter") != IntPtr.Zero;
29-
}
30-
catch
31-
{
32-
_available = false;
33-
}
34-
return _available.Value;
35-
}
36-
}
37-
3820
// ── Operation notifications ────────────────────────────────────────────
3921

4022
public static bool ShowProgress(AbstractOperation operation)
4123
{
42-
if (!IsAvailable() || Settings.AreProgressNotificationsDisabled()) return false;
24+
if (Settings.AreProgressNotificationsDisabled()) return false;
4325
try
4426
{
4527
string title = operation.Metadata.Title.Length > 0
@@ -61,7 +43,7 @@ public static bool ShowProgress(AbstractOperation operation)
6143

6244
public static bool ShowSuccess(AbstractOperation operation)
6345
{
64-
if (!IsAvailable() || Settings.AreSuccessNotificationsDisabled()) return false;
46+
if (Settings.AreSuccessNotificationsDisabled()) return false;
6547
try
6648
{
6749
string title = operation.Metadata.SuccessTitle.Length > 0
@@ -83,7 +65,7 @@ public static bool ShowSuccess(AbstractOperation operation)
8365

8466
public static bool ShowError(AbstractOperation operation)
8567
{
86-
if (!IsAvailable() || Settings.AreErrorNotificationsDisabled()) return false;
68+
if (Settings.AreErrorNotificationsDisabled()) return false;
8769
try
8870
{
8971
string title = operation.Metadata.FailureTitle.Length > 0
@@ -107,7 +89,7 @@ public static bool ShowError(AbstractOperation operation)
10789

10890
public static void ShowUpdatesAvailableNotification(IReadOnlyList<IPackage> upgradable)
10991
{
110-
if (!IsAvailable() || Settings.AreUpdatesNotificationsDisabled()) return;
92+
if (Settings.AreUpdatesNotificationsDisabled()) return;
11193
try
11294
{
11395
string title, message;
@@ -131,9 +113,34 @@ public static void ShowUpdatesAvailableNotification(IReadOnlyList<IPackage> upgr
131113
}
132114
}
133115

116+
public static void ShowUpgradingPackagesNotification(IReadOnlyList<IPackage> upgradable)
117+
{
118+
if (Settings.AreUpdatesNotificationsDisabled()) return;
119+
try
120+
{
121+
string title, message;
122+
if (upgradable.Count == 1)
123+
{
124+
title = CoreTools.Translate("An update was found!");
125+
message = CoreTools.Translate("{0} is being updated to version {1}",
126+
upgradable[0].Name, upgradable[0].NewVersionString);
127+
}
128+
else
129+
{
130+
title = CoreTools.Translate("{0} packages are being updated", upgradable.Count);
131+
message = string.Join(", ", upgradable.Select(p => p.Name));
132+
}
133+
DeliverNotification(title, message);
134+
}
135+
catch (Exception ex)
136+
{
137+
Logger.Warn("macOS upgrading-packages notification failed");
138+
Logger.Warn(ex);
139+
}
140+
}
141+
134142
public static void ShowSelfUpdateAvailableNotification(string newVersion)
135143
{
136-
if (!IsAvailable()) return;
137144
try
138145
{
139146
DeliverNotification(
@@ -149,7 +156,7 @@ public static void ShowSelfUpdateAvailableNotification(string newVersion)
149156

150157
public static void ShowNewShortcutsNotification(IReadOnlyList<string> shortcuts)
151158
{
152-
if (!IsAvailable() || Settings.AreNotificationsDisabled()) return;
159+
if (Settings.AreNotificationsDisabled()) return;
153160
try
154161
{
155162
string title, message;
@@ -180,38 +187,25 @@ public static void ShowNewShortcutsNotification(IReadOnlyList<string> shortcuts)
180187

181188
private static void DeliverNotification(string title, string message)
182189
{
183-
var centerClass = objc_getClass("NSUserNotificationCenter");
184-
var center = MsgSend(centerClass, Sel("defaultUserNotificationCenter"));
185-
186-
var notifClass = objc_getClass("NSUserNotification");
187-
var notif = MsgSend(MsgSend(notifClass, Sel("alloc")), Sel("init"));
188-
189-
MsgSend(notif, Sel("setTitle:"), ToNSString(title));
190-
MsgSend(notif, Sel("setInformativeText:"), ToNSString(message));
191-
MsgSend(center, Sel("deliverNotification:"), notif);
192-
MsgSend(notif, Sel("autorelease"));
193-
}
194-
195-
private static IntPtr ToNSString(string s)
196-
{
197-
IntPtr ptr = Marshal.StringToCoTaskMemUTF8(s);
198-
try
199-
{
200-
return MsgSend(objc_getClass("NSString"), Sel("stringWithUTF8String:"), ptr);
201-
}
202-
finally
190+
// NSUserNotificationCenter was removed in macOS 14; osascript works on all versions.
191+
string script = "display notification " + AppleScriptString(message)
192+
+ " with title " + AppleScriptString(title);
193+
Process.Start(new ProcessStartInfo
203194
{
204-
Marshal.FreeCoTaskMem(ptr);
205-
}
195+
FileName = "/usr/bin/osascript",
196+
ArgumentList = { "-e", script },
197+
UseShellExecute = false,
198+
CreateNoWindow = true,
199+
});
206200
}
207201

208-
private static IntPtr Sel(string name) => sel_registerName(name);
202+
private static string AppleScriptString(string s) =>
203+
"\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
209204

210205
// ── Dock icon ──────────────────────────────────────────────────────────
211206

212207
public static void SetDockIcon(byte[] pngBytes)
213208
{
214-
if (!OperatingSystem.IsMacOS()) return;
215209
try
216210
{
217211
var handle = GCHandle.Alloc(pngBytes, GCHandleType.Pinned);
@@ -241,7 +235,9 @@ public static void SetDockIcon(byte[] pngBytes)
241235
}
242236
}
243237

244-
// ── ObjC runtime P/Invoke ──────────────────────────────────────────────
238+
private static IntPtr Sel(string name) => sel_registerName(name);
239+
240+
// ── ObjC runtime P/Invoke (used by SetDockIcon) ────────────────────────
245241

246242
[DllImport("/usr/lib/libobjc.A.dylib")]
247243
private static extern IntPtr objc_getClass(string name);

src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,58 @@ public static void ShowUpdatesAvailableNotification(IReadOnlyList<IPackage> upgr
211211
}
212212
}
213213

214+
/// <summary>
215+
/// Shows a Windows toast notification when packages are actively being updated (auto-update triggered).
216+
/// </summary>
217+
public static void ShowUpgradingPackagesNotification(IReadOnlyList<IPackage> upgradable)
218+
{
219+
if (!EnsureRegistered()) return;
220+
if (Settings.AreUpdatesNotificationsDisabled()) return;
221+
222+
bool sendNotification = upgradable.Any(p =>
223+
!Settings.GetDictionaryItem<string, bool>(
224+
Settings.K.DisabledPackageManagerNotifications, p.Manager.Name));
225+
if (!sendNotification) return;
226+
227+
try
228+
{
229+
_removeByTagAsyncMethod?.Invoke(_managerDefault,
230+
[CoreData.UpdatesAvailableNotificationTag.ToString()]);
231+
232+
if (upgradable.Count == 1)
233+
{
234+
ShowTextToast(
235+
tag: CoreData.UpdatesAvailableNotificationTag.ToString(),
236+
scenario: ScenarioDefault,
237+
title: CoreTools.Translate("An update was found!"),
238+
message: CoreTools.Translate("{0} is being updated to version {1}",
239+
upgradable[0].Name, upgradable[0].NewVersionString),
240+
suppressDisplay: false,
241+
defaultAction: NotificationArguments.ShowOnUpdatesTab);
242+
}
243+
else
244+
{
245+
string attribution = string.Join(", ", upgradable
246+
.Where(p => !Settings.GetDictionaryItem<string, bool>(
247+
Settings.K.DisabledPackageManagerNotifications, p.Manager.Name))
248+
.Select(p => p.Name));
249+
250+
ShowTextToast(
251+
tag: CoreData.UpdatesAvailableNotificationTag.ToString(),
252+
scenario: ScenarioDefault,
253+
title: CoreTools.Translate("{0} packages are being updated", upgradable.Count),
254+
message: attribution,
255+
suppressDisplay: false,
256+
defaultAction: NotificationArguments.ShowOnUpdatesTab);
257+
}
258+
}
259+
catch (Exception ex)
260+
{
261+
Logger.Warn("Could not show upgrading-packages notification");
262+
Logger.Warn(ex);
263+
}
264+
}
265+
214266
/// <summary>
215267
/// Shows a Windows toast offering a UniGetUI self-update with an "Update now" button.
216268
/// </summary>

src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using UniGetUI.Core.Data;
1818
using UniGetUI.Core.SettingsEngine;
1919
using UniGetUI.Core.Tools;
20+
using UniGetUI.Interface.Enums;
2021
using UniGetUI.PackageEngine;
2122
using UniGetUI.PackageEngine.Enums;
2223
using UniGetUI.PackageEngine.Interfaces;
@@ -150,16 +151,12 @@ public MainWindowViewModel()
150151
Dispatcher.UIThread.Post(() =>
151152
Sidebar.UpdatesBadgeCount = upgLoader.Count());
152153
Sidebar.UpdatesBadgeCount = upgLoader.Count();
153-
154-
upgLoader.FinishedLoading += (_, _) =>
155-
{
156-
var upgradable = upgLoader.Packages.ToList();
157-
if (upgradable.Count == 0) return;
158-
WindowsAppNotificationBridge.ShowUpdatesAvailableNotification(upgradable);
159-
MacOsNotificationBridge.ShowUpdatesAvailableNotification(upgradable);
160-
};
154+
// Notifications and auto-update logic are handled by SoftwareUpdatesPage.WhenPackagesLoaded
161155
}
162156

157+
WindowsAppNotificationBridge.NotificationActivated += action =>
158+
Dispatcher.UIThread.Post(() => HandleNotificationActivation(action));
159+
163160
BundlesPage.UnsavedChangesStateChanged += (_, _) =>
164161
Dispatcher.UIThread.Post(() =>
165162
Sidebar.BundlesBadgeVisible = BundlesPage.HasUnsavedChanges);
@@ -405,6 +402,28 @@ private async Task ShowAboutDialog()
405402
Sidebar.SelectNavButtonForPage(_currentPage);
406403
}
407404

405+
// ─── Notification activation ─────────────────────────────────────────────
406+
private void HandleNotificationActivation(string action)
407+
{
408+
if (action == NotificationArguments.UpdateAllPackages)
409+
{
410+
_ = AvaloniaPackageOperationHelper.UpdateAllAsync();
411+
}
412+
else if (action == NotificationArguments.ShowOnUpdatesTab)
413+
{
414+
NavigateTo(PageType.Updates);
415+
MainWindow.Instance?.ShowFromTray();
416+
}
417+
else if (action == NotificationArguments.Show)
418+
{
419+
MainWindow.Instance?.ShowFromTray();
420+
}
421+
else if (action == NotificationArguments.ReleaseSelfUpdateLock)
422+
{
423+
AvaloniaAutoUpdater.ReleaseLockForAutoupdate_Notification = true;
424+
}
425+
}
426+
408427
// ─── Search box ──────────────────────────────────────────────────────────
409428
[RelayCommand]
410429
public void SubmitGlobalSearch()

0 commit comments

Comments
 (0)